Make domain properties nullable by default (Grails 8)#15721
Make domain properties nullable by default (Grails 8)#15721codeconsole wants to merge 5 commits into
Conversation
45ef5e2 to
f3b9d66
Compare
56e5745 to
7f1ad1f
Compare
Flip GORM's validation default so an unconstrained persistent (domain) property is nullable
unless explicitly constrained, aligning Grails with the rest of the JVM persistence/validation
ecosystem (JPA/Hibernate, Spring Data JPA & MongoDB, Micronaut Data, Jakarta Bean Validation),
all of which treat an unconstrained property as valid-when-null.
The default is controlled by a new YAML-friendly boolean, defaulting to nullable-by-default:
grails.gorm.default.nullable = false # restore legacy required-by-default
This is wired through DefaultConstraintEvaluator (new defaultNullable, default true), surfaced as
ConnectionSourceSettings.DefaultSettings.nullable, and read on both validator-construction paths
(DefaultValidatorRegistry for the GORM datastore and DefaultConstraintEvaluatorFactoryBean for
Grails domain classes). The existing closure form also still works:
grails.gorm.default.constraints = { '*'(nullable: false) }
Scope notes:
- Validation layer only. Command-object validation (Validateable.defaultNullable()) is intentionally
left required-by-default and unchanged.
- Column/DDL nullability is governed separately by the mapping layer (Property.nullable /
GrailsDomainBinder) and is not changed here; aligning the DDL default is a documented follow-up.
Tests: existing specs that assert required-by-default semantics are updated to declare the field(s)
they depend on explicitly (nullable: false), reproducing the prior baseline for just those fields
rather than disabling the new default wholesale. Notes:
- grails-test-suite-uber DomainConstraintGettersSpec restores required-by-default for its own
context via a per-spec doWithConfig override, since it exists to verify default-constraint
enumeration via the nullable error.
- FindOrCreateWhereSpec (mongodb) was using the wrong Person class (a same-package collision); it
now imports grails.gorm.tests.Person to match Pet.owner.
- New NullableByDefaultSpec demonstrates the new default without any opt-out, and verifies that
grails.gorm.default.nullable = false restores required-by-default through DefaultValidatorRegistry.
- The example-app functional/integration suites assert required-by-default app-wide, so each opts out
with grails.gorm.default.nullable = false in its application.yml (the flag's intended use).
Docs: the nullable constraint reference now documents the new default and the
grails.gorm.default.nullable flag (YAML and groovy forms).
7f1ad1f to
c27b930
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15721 +/- ##
==================================================
+ Coverage 48.3185% 48.4341% +0.1155%
- Complexity 15212 15266 +54
==================================================
Files 1875 1875
Lines 85818 85859 +41
Branches 14969 14972 +3
==================================================
+ Hits 41466 41585 +119
+ Misses 37980 37865 -115
- Partials 6372 6409 +37
🚀 New features to boost your workflow:
|
jdaugherty
left a comment
There was a problem hiding this comment.
@jamesfredley created a documentation file for our default values and you're now changing that default, but not changing those files
…etadata Follow-up docs/metadata for the nullable-by-default change: - Upgrade guide (upgrading80x.adoc): new "GORM Properties Are Nullable by Default" section describing the breaking default and the grails.gorm.default.nullable opt-out (YAML and groovy). - Validation constraints guide (constraints.adoc): corrected the now-stale note that claimed all domain properties are not-nullable by default. - grails-core spring-configuration-metadata.json: register grails.gorm.default.nullable so IDEs (IntelliJ) recognise, document and autocomplete it in application.yml.
|
Updated the default-values metadata JSON: |
Resolve upgrade-guide conflict: take 8.0.x's renumbered sections and append the 'GORM Properties Are Nullable by Default' section as #26 (no renumbering).
| "description": "A closure applied as the default constraints for all domain classes.", | ||
| "defaultValue": {} | ||
| }, | ||
| { |
There was a problem hiding this comment.
You added this to grails-core instead of grails-datastore-core.
There was a problem hiding this comment.
@jamesfredley why are we not adding a specific configuration file for the datastore project? That's where the constraint is actually used.
There was a problem hiding this comment.
@jdaugherty there is no similar file in grails-datastore-core. I put it where all the other grails.gorm.* keys are. It wouldn't make sense to put it a location different from where the other keys are.
There was a problem hiding this comment.
I agree, and we shouldn't let this block this change. But I think we've made a larger mistake here - the config keys were supposed to exist in the projects where they're used/defined. It's my understanding that hasn't happened based on this.
|
The configuration metadata for |
- Encapsulate the config read: add ConstraintEvalUtils.getDefaultNullable(Config) (new grails.config.Settings.GORM_DEFAULT_NULLABLE), mirroring getDefaultConstraints, instead of reading the property inline in the factory bean. - DefaultConstraintEvaluatorFactoryBean: pass cacheAutoTimestampAnnotations=true literally again (drop the unrelated config read that changed behaviour) and use the new helper; remove the now-unused Settings import. - Remove the unused datastore Settings.SETTING_DEFAULT_NULLABLE constant. - Drop the redundant per-app comment from the example-app application.yml opt-outs.
✅ All tests passed ✅🏷️ Commit: b112a97 Learn more about TestLens at testlens.app. |
|
I think this can be merged after we discuss in the weekly. This PR identified a configuration issue that I believe we need to discuss before merging. I'd suggest we merge this after Wednesday's meeting. |
|
@jdaugherty but if the hold up is just the json file, why not just approve and I can do a separate PR to address all the data mapping json changes. Unless you want just my 1 change by itself in a json file which I can do here if you want. |
Summary
Change GORM's default constraint so an unconstrained persistent (domain) property is nullable (optional) rather than required. Today Grails applies an implicit
nullable: falseto every persistent property; this PR flips that default tonullable: true.The default lives in
DefaultConstraintEvaluatorand is controlled by a new YAML‑friendly boolean that defaults to nullable‑by‑default:Scope is the validation layer only. Command‑object validation (
Validateable.defaultNullable()) is intentionally left required‑by‑default and unchanged in this PR.Why this should be the Grails 8 default
Grails is the only mainstream JVM data framework that is required‑by‑default. Every comparable layer treats an unconstrained property as nullable and makes you opt into "required":
@Column(nullable=false)/@NotNullto require)@NonNull/ Kotlin non‑null type to require)@NotNullis the opt‑innullable: trueto allow null)The case for flipping it:
@NotNullis the explicit opt‑in. Grails' implicitnullable:falseis a silent, framework‑specific reversal of the spec it otherwise embraces.ValidationExceptionon the first save (often in an unrelated path, e.g. a BootStrap seed). Mixing in reusable field sets is exactly the modular design Grails 8 should encourage; required‑by‑default punishes it.Backward compatibility
Deliberate behavior change, with a one‑line opt‑out to restore the legacy behavior per application. Either set the boolean flag (YAML or groovy):
…or use the existing wildcard‑constraint form, which also still works:
The
grails.gorm.default.constraintsmachinery applies'*'constraints before the framework default and skips the default when one is set (canApplyNullableConstraint), so either opt‑out composes cleanly with per‑property overrides.Command objects are unaffected (still required‑by‑default). If you additionally want command objects to be nullable‑by‑default, that remains an explicit per‑class opt‑in via
static boolean defaultNullable() { true }.Test suite
The affected test suite has been swept and the full
./gradlew testis green. Rather than disabling the new default wholesale, each spec that asserts required‑by‑default semantics now declares the specific field(s) it depends on explicitly (nullable: false), reproducing the prior baseline for just those fields. A module‑widegrails.gorm.default.constraintsopt‑out was deliberately not used as the general mechanism because that closure is also evaluated against the GORM mapping builder and can perturb per‑property mapping (e.g.formula:/derived‑property detection used byf:fields/f:all), and because several specs define their owngrails.gorm.default.constraintsthat a module‑wide file would clobber.A new
NullableByDefaultSpecdemonstrates the change without any opt‑out (an unconstrained property validates while null, while a property with an explicitnullable: falseis still required), and verifies thatgrails.gorm.default.nullable = falserestores required‑by‑default throughDefaultValidatorRegistry.Notes / scope
Property.nullable/GrailsDomainBinder) and is not changed here; aligning the DDL default is a documented follow‑up for maintainers.8.0.x.