Fix bindData binding id/version when domain extends a @DirtyCheck base (#15681)#15699
Fix bindData binding id/version when domain extends a @DirtyCheck base (#15681)#15699jamesfredley wants to merge 3 commits into
Conversation
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
There was a problem hiding this comment.
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
isDomainClassdetection earlier and filter inherited parent whitelist entries againstDOMAIN_CLASS_PROPERTIES_TO_EXCLUDE_BY_DEFAULT. - Add a regression test using a
@DirtyCheckabstract base extended by an@Entitydomain 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
Fixes #15681
Problem
The Grails data binding guide states:
However, when a domain class extends an abstract base class that is not itself recognised as a domain class - for example an abstract
@DirtyCheckclass insrc/main/groovyonto which GORM injectsidandversion(viagetFurthestUnresolvedParent()during joint compilation) -bindData(and the map constructor) would bindidandversionfrom the input.Root cause
In
DefaultASTDatabindingHelper.getPropertyNamesToIncludeInWhiteList():isDomainClass = false(it has no@Entity/@jakarta.persistence.Entityannotation and is not undergrails-app/domain). Because of that,shouldFieldBeInWhiteList()skips theDOMAIN_CLASS_PROPERTIES_TO_EXCLUDE_BY_DEFAULTcheck, soidandversionare included in the base class' whitelist.bindablePropertyNames.bindablePropertyNames.contains(fieldName), soshouldFieldBeInWhiteList()- which would correctly returnfalseforid/versionon a domain class - is never consulted. As a resultidandversionend 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 explicitbindable: trueconstraint 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
DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpecthat bindsid,versionand a regular property to a domain class extending a@DirtyCheckabstract base, asserting thatid/versionare 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-webbinding/command-object/bindData specs and the persistenceGrailsWebDataBinderSpec).