grailsgrails-domain-classgrails-validation

How do I create and XOR validation for two fields in a Grails domain class?


I have an issue where my domain class has two potential mutually exclusive external keys, either a serial number or a legacy lookup value.

Since I'm not sure which one I'll have for any given entry, I've made them both nullable and added custom validation to try to ensure I have one and only one value.

package myproject 

class Sample {

    String information
    String legacyLookup
    String serialNumber

    static constraints = {
        information(nullable: true)
        legacyLookup(nullable: true)
        serialNumber(nullable: true)

        legacyLookup validator: {
            return ((serialNumber != null && legacyLookup == null) || (serialNumber == null && legacyLookup != null))
        }

        serialNumber validator: {
            return ((serialNumber != null && legacyLookup == null) || (serialNumber == null && legacyLookup != null))
        }
    }
}

I created the default CRUD screens and tried to create an entry for this domain class

information: Blah Blah
serialNumber: 
legacyLookup: BLAHINDEX123

This dies in the validator with the following message:

No such property: serialNumber for class: myproject.Sample

What am I missing?


Solution

  • Having each property in there multiple times is not necessary; in fact you only need one of them actually constrained. Also you can't just reference properties directly by their name. There are objects that are passed to the constraint closure that are used to get at the values (see the docs). Probably the simplest way I've found to do this is as follows:

    class Sample {
        String information
        String legacyLookup
        String serialNumber
    
        static constraints = {
            information(nullable: true)
            legacyLookup(validator: {val, obj->
                if( (!val && !obj.serialNumber) || (val && obj.serialNumber) ) {
                    return 'must.be.one'
                }
            })
        }
    }
    

    And then have an entry in the messages.properties file like this:

    must.be.one=Please enter either a serial number or a legacy id - not both
    

    Or you could have separate messages for each condition - both are entered, or both are blank like this:

    legacyLookup(validator: {val, obj->
        if(!val && !obj.serialNumber) {
             return 'must.be.one'
        }
        if(val && obj.serialNumber) { 
             return 'only.one'
        }
    })
    

    And then have two messages in message.properties:

    only.one=Don't fill out both
    must.be.one=Fill out at least one...
    

    You don't need to return anything from the constraint if there is no error...