Skip to content

Fix bindData binding id/version when domain extends a @DirtyCheck base (#15681)#15699

Open
jamesfredley wants to merge 3 commits into
7.0.xfrom
fix/15681-bindData-id-version-dirtycheck-base
Open

Fix bindData binding id/version when domain extends a @DirtyCheck base (#15681)#15699
jamesfredley wants to merge 3 commits into
7.0.xfrom
fix/15681-bindData-id-version-dirtycheck-base

Conversation

@jamesfredley
Copy link
Copy Markdown
Contributor

Fixes #15681

Problem

The Grails data binding guide states:

The "id" and "version" properties of a domain class are never bound by default.

However, when a domain class extends an abstract base class that is not itself recognised as a domain class - for example an abstract @DirtyCheck class in src/main/groovy onto which GORM injects id and version (via getFurthestUnresolvedParent() during joint compilation) - bindData (and the map constructor) would bind id and version from the input.

Root cause

In DefaultASTDatabindingHelper.getPropertyNamesToIncludeInWhiteList():

  1. The abstract base's whitelist is computed with isDomainClass = false (it has no @Entity/@jakarta.persistence.Entity annotation and is not under grails-app/domain). Because of that, shouldFieldBeInWhiteList() skips the DOMAIN_CLASS_PROPERTIES_TO_EXCLUDE_BY_DEFAULT check, so id and version are included in the base class' whitelist.
  2. When the domain subclass is processed, it inherits the parent whitelist into bindablePropertyNames.
  3. The field loop then short-circuits on bindablePropertyNames.contains(fieldName), so shouldFieldBeInWhiteList() - which would correctly return false for id/version on a domain class - is never consulted. As a result id and version end up in the subclass' $defaultDatabindingWhiteList.

Fix

When the current class is a domain class, the default-excluded properties (id, version, dateCreated, lastUpdated) are no longer propagated from the inherited parent whitelist. An explicit bindable: true constraint declared on the domain class itself still re-enables binding, because constraints are processed after the inherited names are filtered.

Tests

Added a regression test to DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec that binds id, version and a regular property to a domain class extending a @DirtyCheck abstract base, asserting that id/version are not bound while the regular property is. The test fails before the fix and passes after.

Verified no regressions across the existing data-binding suites (grails-test-suite-web binding/command-object/bindData specs and the persistence GrailsWebDataBinderSpec).

The id and version properties of a domain class should never be bound
by default. However, when a domain class extends an abstract base class
that is not itself recognised as a domain class (for example an abstract
@DirtyCheck class in src/main/groovy onto which GORM injects id and
version), those properties were incorrectly included in the domain
class' default databinding whitelist.

DefaultASTDatabindingHelper computes the parent class whitelist with
isDomainClass = false for such a base, so shouldFieldBeInWhiteList does
not exclude id and version. The domain subclass then inherits the parent
whitelist into its bindablePropertyNames, and the subsequent field loop
short-circuits on bindablePropertyNames.contains(fieldName), bypassing
the domain-class exclusion entirely.

Filter the inherited parent whitelist so the default-excluded properties
(id, version, dateCreated, lastUpdated) are not propagated into a domain
class' whitelist. An explicit bindable: true constraint on the domain
class itself still re-enables binding, because constraints are processed
after the inherited names are filtered.

Fixes #15681

Assisted-by: claude-code:claude-opus-4-8
Copilot AI review requested due to automatic review settings May 29, 2026 04:36
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a bug where bindData (and domain map constructors) would bind id and version on a domain class that extends an abstract @DirtyCheck base in src/main/groovy. GORM injects id/version into the unresolved parent, which is not recognised as a domain class, so its generated $defaultDatabindingWhiteList includes id/version. The subclass then inherited those names into bindablePropertyNames, short-circuiting the domain-class exclusion in shouldFieldBeInWhiteList. The fix filters the inherited parent whitelist to drop the default-excluded special properties (id, version, dateCreated, lastUpdated) when the current class is itself a domain class.

Changes:

  • Hoist isDomainClass detection earlier and filter inherited parent whitelist entries against DOMAIN_CLASS_PROPERTIES_TO_EXCLUDE_BY_DEFAULT.
  • Add a regression test using a @DirtyCheck abstract base extended by an @Entity domain subclass.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
grails-web-databinding/src/main/groovy/org/grails/web/databinding/DefaultASTDatabindingHelper.java Filters inherited parent whitelist properties to exclude domain special properties for domain classes.
grails-test-suite-web/src/test/groovy/org/grails/web/binding/DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec.groovy Adds regression test for @DirtyCheck abstract base inheritance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Expand the test coverage for the data binding whitelist fix across all
layers:

Unit (DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec):
- id/version not bound when extending a @DirtyCheck abstract base
- inherited dateCreated/lastUpdated not bound on the domain subclass
- regular inherited property is still bound
- explicit bindable:true on inherited special properties re-enables
  binding without affecting id/version
- id/version not bound across multiple levels of abstract inheritance

Controller (DirtyCheckBaseBindDataSpec):
- bindData(domain, params) does not bind id/version (the bug's API)
- documented exclude workaround still prevents id binding
- explicit include list binds only the listed property, never id
- binding a plain Map through bindData does not bind id/version

Functional (grails-test-examples-gorm):
- AbstractDirtyCheckedRecord (@DirtyCheck base in src/main/groovy),
  DirtyCheckedRecord domain and DirtyCheckBindingController reproduce
  the exact scenario from the bug report
- DirtyCheckBindingSpec boots a real application and asserts over HTTP
  that id/version are not bound while regular properties are

Assisted-by: claude-code:claude-opus-4-8
@jamesfredley jamesfredley self-assigned this May 29, 2026
@jamesfredley jamesfredley moved this to In Progress in Apache Grails May 29, 2026
@jamesfredley jamesfredley added this to the grails:7.0.12 milestone May 29, 2026
@jamesfredley jamesfredley requested a review from sbglasius May 29, 2026 14:35
@jamesfredley jamesfredley requested review from dauer and matrei May 29, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

GORM bindData incorrectly binds id when domain class extends a @DirtyCheck abstract base class

3 participants