In this
post, we’ll see how to achieve conditional bean validation in a few steps using
Hibernate Validator.
Thorough explanation on how to implement bean
validation is out of this article's scope, so the reader is assumed to already have practical experience with
bean validation (aka JSR-303).
Example use case
Suppose we have a bean of type ContactInfo.java
that stores user contact information. For simplicity sake, we consider it
only holds a country, a zip code and a phone number. Depending on the user’s
country we may want the zip code to be mandatory or optional.
Here’s our
bean:
package domain; import javax.validation.constraints.NotNull; import validationgroup.USValidation; public class ContactData { private Country country; // ENUM indicating the user's country private String zipCode; private String phoneNumber; public ContactData(Country country, String zipCode, String phoneNumber) { this.country = country; this.phoneNumber = phoneNumber; this.zipCode = zipCode; } public Country getCountry(){ return this.country; } public String getZipCode() { return zipCode; } public String getPhoneNumber() { return phoneNumber; } }
Step 1 - Define a validation group
Create a
marker interface. This will be used as an identifier to a group of validation
rules:
public interface USValidation { }
Step 2 - Add validation rules to our validation group
Add the @Null annotation on the zipCode getter
as follows:
@NotNull(message="Zip code is mandatory", groups={USValidation.class}) public String getZipCode() { return zipCode; }
The groups attribute
specifies the group(s) to which the validation rule belongs to.
Step 3 - Configure the validator
Tell the validator object to apply the
validation rules from our group, in addition to the default ones (which are the
validation rules with no groups attribute specified).
There are actually two ways to configure the
validator:
Solution A
The simplest way is when we got a reference to
the validator object. In that case, we just need to pass it the bean instance
to validate, plus the interface that corresponds to the group we defined at
step 1.
ContactData cd = new ContactData(Country.US, null, null); Set<ConstraintViolation<ContactData>> validationResult = validator.validate(cd); Assert.assertEquals(validationResult.size(), 0);
Solution B
In case we
don’t get a reference to the validator object
(for instance, when we rely on the @Valid Spring annotation to trigger
bean validation from within a Controller), we can define a custom Sequence
Provider to specify which are the validation groups that must be applied.
To define a custom Sequence Provider, we just
need to create a class that implements the DefaultSequenceProvider
interface. This interface exposes a single method that returns a list of
classes that correspond to the validation groups we want to apply.
NOTE: the class type of the bean we want to
validate must be added to the returned list. Otherwise an exception like the
following is thrown:
domain.ContactDataBis must be part of the redefined
default group sequence.
This behavior ensures that the underlying
validator object will get the default validation rules at the very least.
Here’s our
custom Sequence Provider:
public class ContactDataSequenceProvider implements DefaultGroupSequenceProvider<ContactData>{ public List<Class<?>> getValidationGroups(ContactData contactData) { List<Class<?>> sequence = new ArrayList<Class<?>>(); /* * ContactDataBis must be added to the returned list so that the validator gets to know * the default validation rules, at the very least. */ sequence.add(ContactDataBis.class); /* * Here, we can implement a certain logic to determine what are the additional group of rules * that must be applied. */ if(contactData != null && contactData.getCountry() == Country.US){ sequence.add(USValidation.class); } return sequence; } }
Once our custom sequence provider is defined, we
just need to annotate the bean class with @GroupSequenceProvider like this:
@GroupSequenceProvider(value = ContactDataSequenceProvider.class) public class ContactDataBis extends ContactData{ public ContactDataBis(Country country, String zipCode, String phoneNumber) { super(country, zipCode, phoneNumber); } }
Based on this, the validator will look for the validation groups it should apply by executing the getValidationGroups method from our Sequence Provider.
Source code
Source code and running examples (unit tests) are available here.