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.

11 comments:

  1. Love it! Very interesting topics, I hope the incoming comments and suggestion are equally positive. Thank you for sharing this information that is actually helpful.


    matreyastudios
    matreyastudios.com

    ReplyDelete
  2. I just learned your page from a friend and hell yeah I’m
    gonna glance once in awhile on your page to see and read more of your works.

    www.triciajoy.com

    ReplyDelete
  3. Thanks for sharing. Do you happen to know how to enforce validation group for nested field ? Or alternatively to get the root object being validated from DefaultGroupSequenceProvider of nested field ?
    class Child{
    @NotNull
    Stirng someField;
    }
    class Parent{
    @Valid
    Child child;

    bool anotherField;
    }

    I want to enforce someField validation based on 'anotherField' value.


    Thanks

    ReplyDelete
  4. Hi Alexander,

    Sorry for the belated answer. In your situation, I would:

    1° define a custom class-level validation annotation like so:

    @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(value = RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {ValidChildValidator.class})
    public @interface ValidChild {
    String message() default "Child object is not valid duh!";

    Class[] groups() default {};

    Class[] payload() default {};
    }

    public class ValidChildValidator implements ConstraintValidator {


    @Override
    public void initialize(ValidChild constraintAnnotation) {

    }

    @Override
    public boolean isValid(Child value, ConstraintValidatorContext context) {
    if (value.getParent().isValidateChild()) {
    if (value.getSomeField() == null) {
    return false;
    }

    return true;
    } else {
    return true;
    }
    }
    }

    2° Have your Child class hold a reference to the Parent object
    3° Annotate Child class with your custom annotation.

    As a result, when the Child object undergoes bean validation, depending on its Parent object's validateChild attribute value, you could either actually validate the Child object, or simply bypass validation by having the isValid( ) method return true.

    Hope that helps.

    ReplyDelete
    Replies
    1. Thanks for the replay.
      I can't add back reference from child to parent :-(
      I achieved what I needed by passing hints (validation groups) to validate() function from JSR 303 API. Turns out that they are "global" applied for each nested node being validated from root object and this is not the case when you do it "hibernate-specific" way with GroupSequenceProvider
      Thanks for your post again, it helped me a lot.

      Delete
  5. Hi KH Yiu,

    In the ContactSequenceProvider, I am getting the incoming Contact object to be NULL during validation.

    Am I going wrong anywhere ?

    ReplyDelete
  6. No, it's absolutely normal. That's because Hibernate Validation has been implemented that way.
    For more details, you can take a look at how Hibernate Validation's BeanMetaDataImpl.java class has been implemented.

    ReplyDelete
  7. hi if the incoming contact object is null then hoe can we add custom sequence

    ReplyDelete
  8. Hi, i am also getting the incoming contact object as null, so in that case how can the US validation sequence , i am stuck on this problem from two days.if i can the logic in BeanMetaDataImpl there i can find that the getDefauktSequenceProvider is passing null as the bean state.so do i need to configue something ! i need to add sequneces based on condtions

    ReplyDelete
  9. As others have mentioned in the comments, I am also getting the contactData as "null". There is stackoverflow question which has the same problem. COuld you guide us in solving this ? : https://stackoverflow.com/questions/44520306/hibernate-validator-group-sequence-provider-getdefaultsequenceprovider-gets-nul

    ReplyDelete