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:

build.gradle
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.