Skip to content

Clear omitted bindData fields when nullMissing is enabled#15950

Open
jamesfredley wants to merge 1 commit into
8.0.xfrom
fix/binddata-null-missing-stale-data
Open

Clear omitted bindData fields when nullMissing is enabled#15950
jamesfredley wants to merge 1 commit into
8.0.xfrom
fix/binddata-null-missing-stale-data

Conversation

@jamesfredley

@jamesfredley jamesfredley commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

What was found

When updating an existing object through bindData, omitted form fields leave the previous persisted value in place. That is a separate concern from mass assignment:

Problem Impact
Omitted keys are ignored during bind Stale values remain after a partial form post
No opt-in way to treat "missing from request" as "clear this field" Apps cannot express complete-state updates safely
#15808 bundled this with a new secureBindData API Maintainer feedback preferred fixing bindData and splitting concerns

What changed

Area Change
Opt-in flag bindData(target, source, [include: [...], nullMissing: true])
Behavior When nullMissing is true and an include allowlist is provided, allowlisted properties absent from the source are set to null
Default Unchanged: omitted fields still leave existing values when nullMissing is absent/false
Guards Indexed/map path handling avoids crashes on malformed indexes during null-missing traversal
Docs / tests bindData reference, upgrade notes, and focused binding tests

Explicitly out of scope

Topic Where it lives
Mass assignment / default deny / @BindAllowed #15947
Enabling nullMissing by default Not done; remains opt-in

Example

bindData book, params, [include: ['title', 'description'], nullMissing: true]

Verification

  • Focused BindDataMethodTests coverage for nullMissing on/off and allowlist interaction.
  • Does not enable nullMissing globally.

Related

PR Relationship
#15947 Mass-assignment hardening (must be reviewed as the primary binding security change)
#15808 Prior combined approach; this PR carves out the stale-data half

Contributor Checklist

Issue and Scope

  • This PR has no acknowledged issue yet; the background above explains why the change is necessary.
  • This PR addresses opt-in omitted-field clearing only.
  • This PR contains a single focused data-binding behavior change.
  • This PR targets 8.0.x, where behavior-hardening changes are permitted for discussion.

Code Quality

  • I have added or updated tests that cover the changes introduced in this PR.
  • I ran focused verification for the binding tests listed above.
  • The change follows the project's code style and avoids mass reformatting.
  • Generative AI tooling was used as an ai-generated starting point and reviewed before submission.

Licensing and Attribution

  • All contributed code is provided under the Apache License 2.0.
  • I have the necessary rights to submit this contribution and confirm it is my own original work.
  • Generative AI tooling use is labeled on the PR (ai-generated-starting-point).

Documentation

  • User-facing nullMissing behavior is documented.
  • The PR description explains what changed and why.

Assisted-by: Sisyphus:xai/grok-4.5 [gpt-coding]

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]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 nullMissing option plumbed through DataBinderDataBindingUtils, and applies clearing only when an explicit include list 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.

Comment on lines +389 to +403
private static boolean isPropertyAllowedByWhitelist(Object object, String propertyName) {
List bindingIncludeList = getBindingIncludeList(object);
if (bindingIncludeList == null || bindingIncludeList.isEmpty()) {
return true;
}
for (Object includedProperty : bindingIncludeList) {
if (includedProperty instanceof CharSequence) {
String includedPropertyName = includedProperty.toString();
if (propertyName.equals(includedPropertyName) || includedPropertyName.startsWith(propertyName + ".")) {
return true;
}
}
}
return false;
}
@bito-code-review

Copy link
Copy Markdown

The observation regarding isPropertyAllowedByWhitelist is accurate. The current implementation relies on exact matches and simple prefix checks, which fails to account for the wildcard semantics used by the binder (e.g., address.* or address_*). Updating the whitelist evaluation to support these patterns would ensure that nested properties are correctly identified as bindable, allowing nullMissing to function consistently across nested paths.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0.85470% with 348 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.2009%. Comparing base (05393b3) to head (6309f18).
⚠️ Report is 55 commits behind head on 8.0.x.

Files with missing lines Patch % Lines
...roovy/grails/web/databinding/DataBindingUtils.java 0.8547% 346 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@                Coverage Diff                 @@
##                8.0.x     #15950        +/-   ##
==================================================
- Coverage     49.3625%   49.2009%   -0.1616%     
- Complexity      16777      16814        +37     
==================================================
  Files            1986       1993         +7     
  Lines           93336      93854       +518     
  Branches        16337      16467       +130     
==================================================
+ Hits            46073      46177       +104     
- Misses          40134      40541       +407     
- Partials         7129       7136         +7     
Files with missing lines Coverage Δ
...roovy/grails/web/databinding/DataBindingUtils.java 14.8000% <0.8547%> (-33.2000%) ⬇️

... and 23 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@testlens-app

testlens-app Bot commented Jul 10, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: 6309f18
▶️ Tests: 9338 executed
⚪️ Checks: 60/60 completed


Learn more about TestLens at testlens.app.

if (indexedProperty instanceof List) {
List list = (List) indexedProperty;
Integer parsedIndex = parseIndex(index);
return parsedIndex != null && parsedIndex >= 0 && parsedIndex < list.size() ? list.get(parsedIndex) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR appears to be doing more than what it says it does. It appears to have overlap with the other changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants