1. Introduction
The Cascade Validation Plugin provides a small, focused extension to Grails constraints that makes it easy to validate
associated objects (domain associations or command objects) as part of normal validation.
Instead of manually calling validate() on every association, the plugin supplies a cascaded constraint that marks an
association to be validated automatically.
1.1. Upgrading from 4.0.x
Due to a change in Hibernate, there is a name clash with the cascade constraint keyword.
For this reason cascade was renamed to cascaded.
Please update all your domain and command classes accordingly: search for cascade: true (or cascade: false) and
replace it with cascaded: true (or cascaded: false).
The constraint class was renamed from CascadeConstraint to CascadedConstraint, and the registration class from
CascadeConstraintRegistration to CascadedConstraintRegistration.
The previous format for the field name on collections was field.0.childProperty.
Since Grails 7 does cascade validation on domain objects, and the format for errors in that validation is
field[0].childProperty, a configuration property was added to control whether the legacy format is used:
constraints:
cascaded:
legacy: true # defaults to false
1.2. License
This plugin is released under the Apache License, Version 2.0.
1.3. Acknowledgements
This plugin was originally based on a blog post by Eric Kelm, and is used here with Eric’s permission.
Following are the plugin contributors:
1.4. Source code
The full source code for this plugin can be found on GitHub.
For issues, improvements or new features go to the plugin’s GitHub issues.
2. Installation
The plugin is published to Maven Central. Add the dependency to your build configuration:
dependencies {
implementation 'io.github.gpc:cascade-validation:8.0.0-SNAPSHOT'
}
2.1. Compatibility
| Plugin version | Grails version | Java version |
|---|---|---|
8.0.x |
Grails 8 (built against 8.0.0-M5) |
21+ |
7.0.x |
Grails 7 |
17+ |
4.0.x |
Grails 5 and 6 |
11+ |
3. Usage
3.1. General usage
The constraint can be applied in two ways, either as cascaded: true or as a Closure<Boolean>:
class Person {
Address address
List<Phone> phones
boolean shouldValidatePhones
static constraints = {
// Will enable cascade for the Address property
address cascaded: true
// Will enable cascade for all phone numbers if `shouldValidatePhones` is true
phones cascaded: { property, target ->
target.shouldValidatePhones
}
}
}
3.2. Domain example
class Person {
String firstName
String lastName
List<PhoneNumber> phoneNumbers = []
static hasMany = [phoneNumbers: PhoneNumber]
static constraints = {
phoneNumbers cascaded: true
}
}
class PhoneNumber {
String countryCode
String areaCode
String number
String extension
TelephoneType telephoneType
boolean isPrimary
static constraints = {
countryCode nullable: false
areaCode nullable: false
number nullable: false
extension nullable: true
telephoneType nullable: false, cascaded: true
}
}
class TelephoneType {
Boolean countryCodeRecommended
static constraints = {
countryCodeRecommended nullable: false
}
}
When Person.validate() runs, the plugin validates every PhoneNumber instance in the phoneNumbers collection,
because cascaded: true is configured for that property.
When each PhoneNumber is validated, cascaded is applied again to its TelephoneType.
3.3. Service usage
In a data service you can save, and validation is applied automatically:
package cascade
import grails.gorm.services.Service
@Service(Person)
interface PersonDataService {
Person save(Person person)
}
See the test case:
package cascade
import grails.gorm.transactions.Rollback
import grails.testing.mixin.integration.Integration
import spock.lang.Specification
@Integration
@Rollback
class PersonDataServiceSpec extends Specification {
PersonDataService personDataService
void "person save without errors"() {
when:
def result = personDataService.save(new Person(firstName: 'John', lastName: 'Doe'))
then:
result.id
result.firstName == 'John'
result.lastName == 'Doe'
}
}
3.4. Data binding usage
When binding data to grails.validation.Validateable objects (for example via controllers or REST endpoints), calling
validate() — or relying on automatic validation in data-binding flows — makes the CascadedConstraint validate the
marked associations as part of the root object’s validation.
3.4.1. A one-to-one relationship
import grails.validation.Validateable
class ValidateableParent implements Validateable {
ValidateableProperty property
ValidateableProperty anotherProperty
boolean shouldCascade() { true }
static constraints = {
property cascaded: true
anotherProperty cascade: { value, object -> object.shouldCascade() }
}
}
import grails.validation.Validateable
class ValidateableProperty implements Validateable {
String field
static constraints = {
field blank: false
}
}
3.4.2. A one-to-many relationship
import grails.validation.Validateable
class ValidateableParentWithChildList implements Validateable {
List<ValidateableProperty> children
static constraints = {
children cascaded: true
}
}
import grails.validation.Validateable
class ValidateableProperty implements Validateable {
String field
static constraints = {
field blank: false
}
}
3.5. Unit testing
To unit-test your cascaded associations it is important to register the cascaded constraint first.
Add the following to your void setup() {} block:
CascadedConstraintRegistration.register(applicationContext)
See examples/cascade-validation-example/src/test/groovy/cascade/PersonCascadeSpec.groovy for more examples.
4. Release notes
Release notes for every version are published on the GitHub releases page.