7. Defining a Custom Validation Annotation

One other integrity constraint we have to consider is about the values of the year property, which should be in [1459. nextYear] range. At first we may have the intention to use @Min and @Max to specify the range, but we discover soon that is not possible to specify an expression as value of the @Max annotation (as well as for the other annotations too), so we cannot specfiy the nextYear value expression. Sure, we are still able to specify the lower range value (i.e. 1459) by using @Min( value = 1459).

Fortunatelly, the Bean Validation API allows to define custom validation annotations with custom code performing the constraint checks. This means that we are free to express any kind of validation logic in this way. Creating and using a custom validation annotation requires to:

  1. create the annotation interface - we call this UpToNextYear and the corresponding code is shown below:

    @Target( {ElementType.FIELD, ElementType.METHOD})
    @Retention( RetentionPolicy.RUNTIME)
    @Constraint( validatedBy = UpToNextYearImpl.class)
    public @interface UpToNextYear {
      String message() default 
          "The value of year must be between 1459 and next year!";
      Class<?>[] groups() default {};
      Class<? extends Payload>[] payload() default {};
    }

    It requires to define the three builtin methods, message (returns the default key or text message in case the constraint is violated), groups (allows the specification of validation groups, to which this constraint belongs) and payload (used by clients of the Validation API to asign custom payload objects to a constraint - this attribute is not used by the API itself). Notice the @Target annotation, which specifies on which kind of element type we can apply the annotation ( e.g. fields/properties and methods in our case). The @Constraint annotation allows to specify (using the validatedBy property) the implementation class which will perform the validation, i.e. UpToNextYearImpl in our case.

  2. create an implementation class where the validation code is defined:

    public class UpToNextYearImpl implements 
        ConstraintValidator<UpToNextYear, Integer> {
      private Calendar calendar;
    
      @Override
      public void initialize( UpToNextYear arg0) {
        this.calendar = Calendar.getInstance();
        calendar.setTime( new Date());
      }
      @Override
      public boolean isValid( Integer year, 
          ConstraintValidatorContext context) {
        if (year == null || 
            year > this.calendar.get( Calendar.YEAR) + 1) {
          return false;
        }
        return true;
      }
    }

    The implementation class implements the ConstraintValidator interface which requires two Java generics parameters: first is the annotation interface defined before (i.e. UpToNextYear), the second one represents the type of elements, which the validator can handle (i.e. Integer, so implicitly also the compatible primitive type int). The initialize method (as also the name specifies) allows to create all kind of initialization are required for performing the validation check. The isValid method is responsible to perform the validation check: it must returns true if validation passed and false otherwise. The first parameter of the isValid method represents the value (current property or returned method value) to be validated and its type must be compatible with the type defined by the second generics parameter of the ConstraintValidator (i.e. Integer in our case).

  3. annotate the property or method - in our case we like to validate the year property so the corresponding code is:

    @Entity
    @Table( name = "books")
    public class Book {
      // ...
    
      @Min( value = 1459)
      @UpToNextYear
      private Integer year;
    
      //...
    }