Make domain properties nullable by default (Grails 8)#15721
Draft
codeconsole wants to merge 1 commit into
Draft
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
✅ All tests passed ✅🏷️ Commit: c27b930 Learn more about TestLens at testlens.app. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.