Clear omitted bindData fields when nullMissing is enabled - #15950
Clear omitted bindData fields when nullMissing is enabled#15950jamesfredley wants to merge 11 commits into
Conversation
When nullMissing is true and an include allowlist is provided, omitted allowlisted properties are set to null. Default remains leave-unchanged. Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]
There was a problem hiding this comment.
Pull request overview
Adds opt-in “null missing” semantics to bindData so that, when nullMissing: true is provided alongside an explicit include allowlist, included properties omitted from the binding source are actively cleared (null) rather than leaving stale values on the target object. This extends Grails’ web data binding behavior to better support typical “edit/update” form semantics without enabling the behavior by default.
Changes:
- Introduces a
nullMissingoption plumbed throughDataBinder→DataBindingUtils, and applies clearing only when an explicitincludelist is provided. - Implements missing-field clearing logic in
DataBindingUtils(including nested indexed collection paths and map-indexed paths). - Adds test coverage and updates documentation + upgrading notes to describe the new opt-in behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBindingUtils.java | Adds nullMissing overloads and implements missing included-property clearing logic after binding. |
| grails-web-databinding/src/main/groovy/grails/web/databinding/DataBinder.groovy | Wires nullMissing: true from the bindData options map into the binding call. |
| grails-test-suite-web/src/test/groovy/org/grails/web/servlet/BindDataMethodTests.groovy | Adds new controller-backed specs covering nullMissing clearing, excludes, nested indexed paths, map paths, and bindable whitelist interactions. |
| grails-doc/src/en/ref/Controllers/bindData.adoc | Documents nullMissing usage and constraints (opt-in; requires include). |
| grails-doc/src/en/guide/upgrading/upgrading80x.adoc | Notes the behavior change for Grails 8.x upgrades (opt-in; only with include). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The observation regarding |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## fix/binddata-mass-assignment #15950 +/- ##
======================================================================
- Coverage 51.8932% 51.5762% -0.3170%
- Complexity 18114 18214 +100
======================================================================
Files 2038 2048 +10
Lines 96213 97227 +1014
Branches 16778 16995 +217
======================================================================
+ Hits 49928 50146 +218
- Misses 38899 39654 +755
- Partials 7386 7427 +41
🚀 New features to boost your workflow:
|
Preserve the strict binding allowlist while adding opt-in stale-data clearing, including indexed paths and explicit include handling. Assisted-by: opencode:gpt-5.6-sol
|
Final update pushed in The branch now incorporates #15947 as its deny-by-default security base and keeps Verification passed for |
Assisted-by: opencode:gpt-5.6-sol
…le-data Assisted-by: opencode:gpt-5.6-sol
|
Thanks for splitting this out — since #15950 is branched directly off #15947, its diff currently duplicates all of #15947's changes on top of the Would you be open to one of:
Whichever is easier on your end — just flagging so we don't duplicate review effort. |
|
PR updated to target fix/binddata-mass-assignment |
jdaugherty
left a comment
There was a problem hiding this comment.
The nullMissing contract is well scoped — opt-in, requires an explicit include, defaults unchanged — and the reference docs read clearly.
Two structural concerns before this lands.
First, the clearing pass re-derives "which included properties were absent from the source" by re-parsing the binding source after binding has already finished. That is why DataBindingUtils grows roughly 640 lines of bespoke path handling, duplicating traversal the binder just performed and already has type information for.
Second, every nullMissing spec inherits legacyBindableDefault=false from the BindDataMethodTests setup(), so the feature has no coverage under the shipping default — even though the docs present it as a plain bindData option with no mention of the binding mode.
Smaller items inline.
| return bindingResult; | ||
| } | ||
|
|
||
| private static void assignNullToMissingIncludedProperties(Object object, DataBindingSource bindingSource, List include, List exclude, String filter) { |
There was a problem hiding this comment.
This is where the cost of the design shows up. assignNullToMissingIncludedProperties re-walks the binding source after grailsWebDataBinder.bind(...) has returned, and to do that it re-implements path splitting (splitPropertyPath, propertyPathSeparator), bracket and index parsing, checkbox marker names, collection and map expansion, and prefix-filter handling — a second implementation of the grammar the binder just traversed with IndexedPropertyReferenceDescriptor and processIndexedProperty.
Two implementations of the same path grammar will drift, and this is the copy that decides which properties get written to. shouldExpandMapEntries / isStructuredMapValueType inferring "is this a nested object map?" from the package name of the value type is a symptom of working without the type information the binder had already resolved.
Could the clearing be driven from inside the binder instead — it already holds the resolved allowlist and knows which properties it set, so the remainder is the set to null? If that is not workable, please extract this into its own collaborator with its own unit tests rather than growing DataBindingUtils, which is already carrying the include-list resolution and the mode switch.
| return null; | ||
| } | ||
|
|
||
| private static void setPropertyValueToNull(Object object, String propertyName) { |
There was a problem hiding this comment.
Every failure here is swallowed. A nullMissing clear that cannot be applied — a primitive-typed property, a property with no setter, a setter that throws — silently leaves the stale value in place, which is exactly the outcome the option exists to prevent, and nothing is recorded in the BindingResult that bindData returns.
The catch comment says "ignore invalid indexed nullMissing paths", but the try also covers the non-indexed mc.setProperty(...) branch, so it is broader than the comment claims.
Please narrow the catch to the path/index parsing, decide explicitly what a primitive-typed include should do (reject it at the API, or set the type default), and surface a failure to clear through the errors object the way a binding failure is surfaced. A test with a primitive property in the include list would pin whichever behavior you choose.
| private static final String BLANK = ""; | ||
| private static final Map<Class, List> CLASS_TO_BINDING_INCLUDE_LIST = new ConcurrentHashMap<>(); | ||
| private static final Map<Class, List> CLASS_TO_LEGACY_BINDING_INCLUDE_LIST = new ConcurrentHashMap<>(); | ||
| private static final Set<String> FRAMEWORK_MANAGED_PROPERTIES = Set.of( |
There was a problem hiding this comment.
This is now the third list of "properties the binder must never touch" in the stack, and the three disagree:
SimpleDataBinder.isFrameworkProperty:class, classLoader, protectionDomain, metaClass, metaPropertyValues, propertiesGrailsWebDataBinder.FRAMEWORK_MANAGED_PROPERTIES:class, errors, id, version, dateCreated, lastUpdated- this one: the union of both
A property added to one and not the others becomes clearable but not bindable, or the reverse. Please consolidate to a single constant that all three consume.
| return bindObjectToInstance(object, source, include, exclude, filter, false); | ||
| } | ||
|
|
||
| public static BindingResult bindObjectToInstance(Object object, Object source, List include, List exclude, String filter, boolean nullMissing) { |
There was a problem hiding this comment.
This overload and the matching bindObjectToDomainInstance(..., boolean nullMissing) are new public API on a class where the neighbouring overloads are documented.
Please add javadoc covering the parameter, the fact that it is ignored unless the caller supplied a non-null include, and that the clearing happens after binding completes — outside the listener callbacks and outside the BindingResult the method returns.
| whiteList.any { item -> item?.toString()?.startsWith(propName + '.') }) | ||
| } | ||
|
|
||
| static boolean isPropertyExcluded(String propertyName, List excludeList) { |
There was a problem hiding this comment.
This adds a public static method to grails.databinding.SimpleDataBinder — public API in grails-databinding-core — solely so DataBindingUtils in grails-web-databinding can call it. It also implements exclusion semantics (.* and _* prefix matching, and nested-path prefixes) that the binder's own isOkToBind does not apply, so SimpleDataBinder now exposes two different notions of "excluded".
It has no javadoc and no coverage in SimpleDataBinderSpec. If it is only needed by the web binding layer, please make it package-private there or move it to a shared internal utility; if it is genuinely meant to be public API, document it and test it at the SimpleDataBinder level.
| target.email == null | ||
| } | ||
|
|
||
| void 'Test bindData With Null Missing Clears Omitted Included Field'() { |
There was a problem hiding this comment.
This spec and every other nullMissing case in the file inherit legacyBindableDefault=false from the setup() at the top, so the whole feature is only exercised in the opt-in secure mode.
The reference docs and the 8.0.x upgrade note both present nullMissing as a plain bindData option with no mention of the binding mode, and the authorization path genuinely differs between the two: isNullMissingPropertyBindable consults getBindingIncludeList(object), which resolves the legacy allowlist under the default and the generated one under secure mode.
Please add default-mode coverage for the main paths — clearing an omitted included field, leaving an excluded one, honouring bindable: false, and the nested/indexed cases — so the documented behavior is verified in the mode most applications will be running.
…ssing-stale-data Preserve permissive compatibility binding and the nullMissing stale-data clearing behavior while resolving overlapping documentation, tests, and binding utilities. Assisted-by: opencode:gpt-5.6-sol
Use one shared property-name set across the core and web binders while preserving the internal bind-all marker across package boundaries. Assisted-by: opencode:gpt-5.6-sol
Move omitted-property path handling into a dedicated collaborator, preserve existing binding errors, report clear failures, and reset primitive properties to type defaults. Assisted-by: opencode:gpt-5.6-sol
Remove the duplicate specification cleanup that referenced per-feature state and prevented the merged test source from compiling. Assisted-by: opencode:gpt-5.6-sol
SimpleDataBinder must only hard-deny intrinsic runtime properties. Grails-managed id/version/dateCreated/lastUpdated/errors remain excluded from default allowlists and nullMissing clearing, but can still bind when explicitly allowed (bindable: true). Assisted-by: Sisyphus:xai/grok-4.5
|
@jdaugherty Addressed after merging the updated #15947 base ( Review responses
Verification
|
🚨 TestLens detected 40 failed tests 🚨Here is what you can do:
Test SummaryCI / Build Grails-Core (macos-latest, 21) > :grails-test-suite-persistence:test
CI / Build Grails-Core (ubuntu-latest, 21) > :grails-test-suite-persistence:test
CI / Build Grails-Core (ubuntu-latest, 25) > :grails-test-suite-persistence:test
CI / Build Grails-Core (windows-latest, 25) > :grails-test-suite-persistence:test
CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21) > :grails-test-suite-persistence:test
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-acl-functional-test-app:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-core-misc-functional-test-app-group:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-core-misc-functional-test-app-roles:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ldap-custom-user-details-context-mapper:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ldap-functional-test-app:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ldap-retrieve-db-roles:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ldap-retrieve-group-roles:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ui-extended:integrationTest
CI / Spring Security Tests (21) > :grails-test-examples-spring-security-ui-simple:integrationTest
🏷️ Commit: 50714be Test Failures (first 10 of 40)GrailsWebDataBinderSpec > Test Map constructor fallback fails closed for a narrow explicit include (:grails-test-suite-persistence:test in CI / Build Grails-Core (macos-latest, 21))GrailsWebDataBinderSpec > Test typed Map binding in default mode notifies listeners and records value conversion errors (:grails-test-suite-persistence:test in CI / Build Grails-Core (macos-latest, 21))GrailsWebDataBinderSpec > Test Map constructor fallback fails closed for a narrow explicit include (:grails-test-suite-persistence:test in CI / Build Grails-Core (ubuntu-latest, 21))GrailsWebDataBinderSpec > Test typed Map binding in default mode notifies listeners and records value conversion errors (:grails-test-suite-persistence:test in CI / Build Grails-Core (ubuntu-latest, 21))GrailsWebDataBinderSpec > Test Map constructor fallback fails closed for a narrow explicit include (:grails-test-suite-persistence:test in CI / Build Grails-Core (ubuntu-latest, 25))GrailsWebDataBinderSpec > Test typed Map binding in default mode notifies listeners and records value conversion errors (:grails-test-suite-persistence:test in CI / Build Grails-Core (ubuntu-latest, 25))GrailsWebDataBinderSpec > Test Map constructor fallback fails closed for a narrow explicit include (:grails-test-suite-persistence:test in CI / Build Grails-Core (windows-latest, 25))GrailsWebDataBinderSpec > Test typed Map binding in default mode notifies listeners and records value conversion errors (:grails-test-suite-persistence:test in CI / Build Grails-Core (windows-latest, 25))GrailsWebDataBinderSpec > Test Map constructor fallback fails closed for a narrow explicit include (:grails-test-suite-persistence:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))GrailsWebDataBinderSpec > Test typed Map binding in default mode notifies listeners and records value conversion errors (:grails-test-suite-persistence:test in CI / Build Grails-Core Rerunning all Tasks (ubuntu-latest, 21))Muted Tests (first 20 of 40)Select tests to mute in this pull request:
Reuse successful test results:
Click the checkbox to trigger a rerun:
Learn more about TestLens at testlens.app. |
Description
Adds opt-in stale-data clearing to
bindData, stacked on the binding base from #15947.When
nullMissing: trueis supplied together with an explicitincludelist, included properties omitted from the source are cleared (reference types tonull, primitives to their type default). Existing behavior is unchanged when the option is absent, false, or no explicit include was supplied.Final contract
bindData(target, source, [include: [...], nullMissing: true])bindable: false, and framework-managed properties remain protected0/false)BindingResultnullMissing; it cannot broaden normal request bindingnullMissing = falseImplementation notes
NullMissingPropertyClearer(not expanded public API).FrameworkPropertyNames(intrinsic runtime vs Grails-managed).bindable: falsealways honored.Example
Verification
:grails-web-databinding:test:grails-databinding-core:testBindDataMethodTests(default-mode and secure-mode nullMissing paths, primitives, BindingResult errors, nested/indexed/maps, excludes,bindable: false)DefaultASTDatabindingHelperDomainClassSpecialPropertiesSpec(explicit bindable special properties)Related
Contributor Checklist
bindDatareference and Grails 8 upgrade guideai-generated-starting-pointAssisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]