Sunday, April 13, 2014

Conditional bean validation using Hibernate Validator

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.