Grails cascade validation for POGOs

Recently, I have been working on some code that does not use any of the GORM capabilities in Grails, but instead controllers call to a service layer which interacts with web services hosted on an ESB. The domain classes that are in use are not understood by Grails to be the domain artefact type and as such, do not inherit certain traits. One of the traits that is missing is the ability to cascade validation from the object being validated down to any child objects. Let me illustrate.

Let’s say that we have a class Person which has some properties. One of these is a residential address, which is of type Address.

import grails.validation.Validateable

@Validateable
class Person {
    String firstName
    String lastName
    Address residenceAddress

    static constraints = {
        firstName  blank: false //other constraints
        lastName   blank: false //other constraints
    }
}

@Validateable
class Address {
    String line1
    String line2
    String city
    String state
    String postalCode

    static constraints = {
        line1       blank: false //other constraints
        city        blank: false //other constraints
        state       blank: false //other constraints
        postalCode  blank: false //other constraints
    }
}

Since the Person class is a POGO and not a Grails domain artefact, the first thing we need to do to make this class capable of being validated during Grails controller binding is to mark the class with @Validateable. Then we can add the static constraints block and it will be used to evaluate/validate the properties during binding.

Issues arise when we need to validate the residenceAddress property of Person. How can we accomplish this? One way to do this is to manually validate the properties, and report any errors.

class MyController {
    // ...
    def personSubmit(Person p) {
        // ...
        // p is already validated, except for residenceAddress
        def anyErrors = p.hasErrors() ||
                        !p.residenceAddress.validate()
        if(anyErrors) {
            // report errors ...
        }
    }
}

The problem with the above approach is two-fold. First, it doesn’t scale. We shouldn’t be validating the parent object and then again validating a child property. Imagine if this class had multiple child properties that were themselves object that again contained properties that were objects. What a nightmare to validate. Second, since you are manually invoking the validate method out of sequence with what is in the constraints block, any errors are reported back within their own object and there is no way to know what the original sequence was for the errors. This is particularly annoying when you need to display errors to a user that were part of a form submission and they are listed out in an order inconsistent with the layout of the form.

So, I decided that I would write some code that would would provide the functionality that is missing with POGOs.

import org.codehaus.groovy.grails.validation.*
import org.springframework.validation.*

class CascadeValidationConstraint extends AbstractVetoingConstraint {

    public static final String NAME = "cascadeValidation"

    @Override
    String getName() {
        NAME
    }

    @Override
    boolean supports(Class type) {
        true
    }

    @Override
    public void setParameter(Object constraintParameter) {
        if (!(constraintParameter instanceof Boolean)) {
            throw new IllegalArgumentException(
                """Parameter for constraint [$name] of
                   property [$constraintPropertyName]
                   of class [$constraintOwningClass]
                   must be a Boolean
                """
            )
        }
        super.setParameter(constraintParameter)
    }

    @Override
    protected boolean skipNullValues() {
        return true
    }

    @Override
    protected boolean processValidateWithVetoing(
            Object target, Object propertyValue,
            Errors errors) {
        if (!propertyValue.validate()) {
            propertyValue.errors.fieldErrors.each {
                String field = "${propertyName}.${it.field}"
                def fieldError = new FieldError(
                    target.errors.objectName,
                    field,
                    it.rejectedValue,
                    it.bindingFailure,
                    it.codes,
                    it.arguments,
                    it.defaultMessage
                )
                errors.addError(fieldError)
            }
            return false
        }
        return true
    }
}

What the above constraint does is validate the given property, and if it doesn’t pass validation according to the constraints block defined on that class, adds each error as a FieldError to the property’s parent object at the correct field location. Then you just need to register your custom constraint. You can do this in Config.groovy, Bootstrap.groovy, or during plug-in initialization if you build this into a plug-in.

import org.codehaus.groovy.grails.validation

ConstrainedProperty.registerNewConstraint(
    CascadeValidationConstraint.NAME,
    CascadeValidationConstraint.class
)

Now our Person class can be defined as follows and all validations are handled as expected (note the highlighted line below).

import grails.validation.Validateable

@Validateable
class Person {
    String firstName
    String lastName
    Address residenceAddress

    static constraints = {
        firstName        blank: false //other constraints
        lastName         blank: false //other constraints
        residenceAddress cascadeValidation: true
    }
}

@Validateable
class Address {
    String line1
    String line2
    String city
    String state
    String postalCode

    static constraints = {
        line1       blank: false //other constraints
        city        blank: false //other constraints
        state       blank: false //other constraints
        postalCode  blank: false //other constraints
    }
}
This entry was posted in Grails and tagged , , , . Bookmark the permalink.

10 Responses to Grails cascade validation for POGOs

  1. vasya10 says:

    neat !

  2. Eric,
    I would like to publish a small Grails plugin based on your code, with a few minor changes (ex: I’ve updated the supports() method to check whether a validate() method exists on the target type). I’ve credited you in the source code and the plugin description. Let me know if it’s OK to use your code and I will put in a request to add it to the plugin repo.
    Thanks

  3. The plugin was approved! Here’s the updated URL:
    http://grails.org/plugin/cascade-validation

    FYI… I discovered when trying to implement a different custom constraint that the constraint may not get attached to Hibernate-enabled domain objects when registered in BootStrap.groovy. It looks like BootStrap.groovy is too late in the config process; the list of constraints on each property is already built up by the time BootStrap.groovy runs. I moved it to Config.groovy and that fixed the issue.

    • asoftwareguy says:

      Thanks for the update! On the Bootstrap vs Config setting, I didn’t run into that issue because on the project where I used it, it didn’t use Hibernate domain classes. I will keep note of that for the future.

  4. Prasanna says:

    I am almost a newbie to Grails. Is there a way I can use this for a list of Command objects inside the parent. As shown below Person has a list of Account and how can i set cascadeValidation property to this.
    class Person {
    String firstName
    String lastName
    Address residenceAddress
    List bankAccounts

    static constraints = {
    firstName blank: false //other constraints
    lastName blank: false //other constraints
    }
    }

  5. Bruno Moura says:

    I know it may be a while, but I think there’s a mispelling when registering the custom validator.

    import org.codehaus.groovy.grails.validation

    ConstrainedProperty.registerNewConstraint(
    *CascadeValidationConstraint*.NAME,
    *CascadeValidationConstraint*.class
    )

    The plugin is hella cool, congrats!!

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.