Custom Development

Creating Custom Validation Constraints

Mitch Goldstein

Background

The Java Validation API contains a number of predefined annotations that can be used for most validation applications. The API allows for creation of custom validations to enable developers to create specialized implementations.

I had encountered an interesting validation requirement that requires restriction of an entered numeric value within a defined range. If the range is well-defined at compile time, it is possible to construct this using the @Min and @Max constraints. To ensure an integer value has a value between one and one-hundred, we could code an accessor method like this:

    @Min(value=1, message="Min value is 1")
    @Max(value=100,  message="Max value is 100")
    public int getRating() {
        return this._rating;
    }

What if the value of the rating depended on other values that could not be bound at compile time? We cannot substitute values into the annotations, so we must create our own custom annotation for this requirement.

For this example, we will create an object with three integer properties: one for the minimal value, one for the maximal value and one for the rating value, which we are restricting to be in between the minimal and maximal values.

public class Rating {
    @Min(value=1, message="Min value is 1")
    @Max(value=100,  message="Max value is 100")
    public int getMinRating() {
        return this._minRating;
    }

    @Min(value=1, message="Min value is 1")
    @Max(value=100,  message="Max value is 100")
    public int getMaxRating() {
        return this._maxRating;
    }

    // TODO: Custom constraint validator on the class to ensure
    // the rating is between MinRating and MaxRating at runtime
    public int getRating() {
        return this._rating;
    }
}

In order to manage constraint values across multiple properties, we cannot use a method validator such as @Min and @Max. We will create a validator that operates on the entire Rating class.

Creating a Custom Constraint Annotation

Annotations in Java are created as a special kind of interface, using the @interface keyword. This interface declaration is recognized by the compiler and an anonymous implementation class and instance are created by run-time code. Each member of the interface is interpreted to represent one of the required or optional parameters to the custom constraint annotation. For our purposes, all we will need to declare is the message parameter, which will provide a message that can be consumed and reported by the validation code.

package com.summa.sip;
// imports omitted...

@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = RatingInRangeValidator.class)
public @interface RatingInRange  {
    String message() default "Rating not in range";

    // Required by validation runtime
    Class<?>[] groups() default {};

    // Required by validation runtime
    Class<? extends Payload>[] payload() default {};
}

The @Target annotation specifies to what sort of program element that the validation applies. It takes a single or list of values of the type ElementType, but we are only allowing our custom annotation to be applied to types, whether they be classes or interfaces. Using this annotation for any other purpose will result in a compiler error.

The @Retention annotation informs the compiler where to keep the annotation information. It takes a single value of type RetentionPolicy. The value can be on of:

  • RUNTIME, meaning the annotation information is available at run-time. We have chosen this because the evaluation of the validity of our datum takes place in run-time and we need to take advantage of reflection to invoke the validation.
  • CLASS, meaning the annotations are recorded in the class file but not retained at run-time (the default policy)
  • SOURCE, meaning the annotation is just for use while editing the source code and is not retained anywhere else

 

The @Constraint annotation is what connects our annotation interface to our validation class. It must implement the ConstraintValidator interface that matches the custom annotation type and the target of the data being validated.

The message declaration is required and must have a non-null default message for the validator to function. Failure to do so will result in a run-time exception.

The other two member declarations are groups and payload and they are required by the validation run-time implementation. Failure to include either of these will also result in a run-time exception.

Creating a Constraint Validator Class

After creating the annotation interface, we must implement the class that actually perform the validation. The name of the validator class must be the same as used in the @Constraint value on the annotation interface and must be accessible to the annotation interface by living in the same package or by public access to the implementation.

The ConstraintValidator<A extends Annotation, T> interface requires two generic parameters:

  • First, the type representing the custom annotation interface, in this example it is RatingInRange.
  • Second, the type that is being validated, in this case Rating.

The interface requires implementation of two public methods:

  • void initialize(A constraintAnnotation) - this method is used to initialize any internal information that the validator might need to do its work. It is required, but can be left as a no-op (empty) implementation.
  • boolean isValid(T value, ConstraintValidatorContext context) - this is the method that actual does the validation. If it returns true, the validation is considered to be successful. The context parameter is a reference to an object that allows the validator to push more information about the validation failure to the implementor if it does not pass.
package com.summa.sip;
// imports omitted...

public class RatingInRangeValidator implements
          ConstraintValidator&lt;RatingInRange, Rating&gt; {

    public void initialize(RatingInRange annotation) {
        // no-op - no initialization needed
    }

    public boolean isValid(Rating rating,
                  ConstraintValidatorContext context) {
        // dirty work goes here
    }
}

Implementing Validator Logic

Now we can implement our logic:

    public boolean isValid(Rating rating,
                    ConstraintValidatorContext context) {
        // If the rating is less than minimum or greater
        // than maximum, return false.
        if (rating.getRating() &lt; rating.getMinRating()  ||
                  rating.getRating() &gt; rating.getMaxRating()) {
            return false;
        }

        // Validation succeeded!
        return true;
    }

It is important to remember a few principles:

  • There is no guarantee in which order the validations will occur, so we can't depend on any other validation either having passed or being guaranteed to pass.
  • The validation message will always minimally consist of the value of the message parameter to the annotation. If none is specified, the default message from the annotation declaration can be used.
  • The failure condition can be enhanced by using the ConstraintValidatorContext. This interface provides access to a ConstraintViolationBuilder, which is used to tack on additional information to the return message. Consult the public documentation for more information on this interface.

Finishing Touches

With our implementation complete, the last thing we need to do is annotate the Rating class with our custom annotation:

 @Entity                 // Mark this as a mapped entity
 @RatingInRange          // Type-level validation
 public class Rating {
     // implemenation omitted
 }

The validation will be triggered automatically when any Rating object is persisted through Hibernate/JPA. JPA 2.0 supports methods that invoke the validation API automatically throughout the life cycle changes of a mapped instance.

Mitch Goldstein
ABOUT THE AUTHOR

Senior Technical Consultant Mitch Goldstein is Summa's first Technical Agile Coach as well as a certified SAFe Program Consultant, combining centuries of technical experience and a great love for all things lean and agile. Mitch has been a featured speaker at technical conferences in the US and Europe as well as a published author and technical journalist. Try to imagine a polymath, but only up to the easy part of calculus. Dabbles in Freemasonry.