This is the second part of a series of blogs about Bean Validation. For a general introduction, read this entry first. This part focuses on constraint definition.

While Bean Validation will come with a set of predefined basic constraints (like @NotNull, @Length and so on), a key feature of the specification is its extensibility. Application developers can and are strongly encouraged to write their own custom constraints matching a particular business requirement.

Writing a custom constraint

Because writing custom constraints is a core part of the specification goal, great care have been taken to make it as simple as possible. Let's walk through the process of creating a custom constraint.

As we have seen in the previous blog entry, a constraint is composed of

  • an annotation
  • an implementation

Constraint annotation

Every constraint is associated to an annotation. You can see it as a type safe alias and a descriptor. Constraint annotations can also hold one or several parameters that will help customize the behavior at declaration time

public class Order {
    @NotNull @OrderNumber private String number;
    @Range(min=0) private BigDecimal totalPrice;
    ...
}

Let's have a look at the @OrderNumber annotation definition

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@ConstraintValidator(OrderNumberValidator.class)
public @interface OrderNumber {
    String message() default "{error.orderNumber}"; 
    String[] groups() default {}; 
}

Constraint annotations are just regular annotations with a few extra things:

  • they must use the runtime retention policy: the bean validation provider will inspect your objects at runtime
  • they must be annotated with @ConstraintValidator
  • they must have a message attribute
  • they must have a groups attribute

The @ConstraintValidator indicates to the bean validation provider that an annotation is a constraint annotation. It also points to the constraint validation implementation routine (we will describe that in a minute).

The message attribute (which generally is defaulted to a key) provides the ability for a constraint declaration to override the default message returned in the constraint error list. We will cover this particular topic in a later post.

groups lets a constraint declaration define the subset of constraints it participates to. Groups enable partial validation and ordered validation. We will cover this particular topic in a later post.

In addition to these mandatory attributes, an annotation can define any additional element to parameterize the constraint logic. The set of parameters is passed to the constraint implementation. For example, a @Range annotation needs min and max attributes.

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@ConstraintValidator(RangeValidator.class)
public @interface Range {
        long max() default Long.MAX_VALUE;

        long min() default Long.MIN_VALUE;

        String message() default "{error.range}";
        String[] groups() default {}; 
}

Now that we have a way to express a constraint and its parameters, we need to provide the logic to validate the constraint.

Constraint implementation

The constraint implementation is associated to its annotation through the use of @ConstraintValidator. In the first early draft, @ValidatorClass is sometimes used in lieu of @ConstraintValidator: this is a mistake driven by a last minute change, sorry. The implementation must implement a very simple interface Constraint<A extends Annotation> where A is the targeted constraint annotation

public class OrderNumberValidator implements Constraint<OrderNumber> {
        public void initialize(OrderNumber constraintAnnotation) {
                //no initialization needed
        }

        /**
         * Order number are of the form Nnnn-nnn-nnn when n is a digit
         * The sum of each nnn numbers must be a multiple of 3
         */
        public boolean isValid(Object object) {
                if ( object == null) return true;
                if ( ! (object instanceof String) )
                        throw new IllegalArgumentException("@OrderNumber only applies to String");
                String orderNumber = (String) object;
                if ( orderNumber.length() != 12 ) return false;
                if ( orderNumber.charAt( 0 ) != 'N'
                                || orderNumber.charAt( 4 ) != '-'
                                || orderNumber.charAt( 8 ) != '-'
                                ) return false;
                try {
                        long result = Integer.parseInt( orderNumber.substring( 1, 4 ) )
                                        + Integer.parseInt( orderNumber.substring( 5, 8 ) )
                                        + Integer.parseInt( orderNumber.substring( 9, 12 ) );
                        return result % 3 == 0;
                }
                catch (NumberFormatException nfe) {
                        return false;
                }
        }
}

The initialize method receives the constraint annotation as a parameter. This method typically does:

  • prepare parameters for the isValid method
  • acquire external resources if needed

As you can see the interface entirely focuses on validation and leaves other concerns such as error rendering to the bean validation provider.

isValid is responsible for validating a value. A few interesting things can be noted:

  • isValid must support concurrent calls
  • exceptions should be raised when the object type received does not match the validation implementation expectations
  • the null value is not considered invalid: the specification recommends to split the core constraint validation from the not-null constraint validation and use @NotNull if the property must not be null

This easily customizable approach gives application programmers the freedom they need to express constraints and validate them.

Applying the same constraint type multiple times

Especially when using groups, you will sometimes need to apply the same type of constraint multiple times on the same element. The Bean Validation specification takes into account annotations containing an array of constraint annotations:

@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Patterns {
        Pattern[] value();
}

@ConstraintValidator(PatternValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface Pattern {
        /** regular expression */
        String regex();

        /** regular expression processing flags */
        int flags() default 0;

        String message() default "{validator.pattern}";
        
        String[] groups() default {};
}

In this example, you can apply multiple patterns on the same property

public class Engine {
        @Patterns( {
            @Pattern(regex = "^[A-Z0-9-]+$", message = "must contain alphabetical characters only"),
            @Pattern(regex = "^....-....-....$", message="must match ....-....-....")
                        } )
        private String serialNumber;
        ...

Building constraints

By default, Bean Validation providers instantiate constraint validation implementations using a no-arg constructor. However, the specification offers an extension point to delegate the instantiation process to a dependency management library such as Web Beans, Guice, Spring, JBoss Seam or even the JBoss Microcontainer.

Depending on the capacity of the dependency management tool, we expect validation implementations to be able to receive injected resources if needed: this mechanism will be entirely dependent on the dependency management tool.

Class-level constraints

Some of you have expressed concerns about the ability to apply a constraint spanning multiple properties, or to express constraint which depend on several properties. The classical example is address validation. Addresses have intricate rules:

  • a street name is somewhat standard and must certainly have a length limit
  • the zip code structure entirely depends on the country
  • the city can often be correlated to a zipcode and some error checking can be done (provided that a validation service is accessible)
  • because of these interdependencies a simple property level constraint does to fit the bill

The solution offered by the Bean Validation specification is two-fold:

  • it offers the ability to force a set of constraints to be applied before an other set of constraints through the use of groups and group sequences. This subject will be covered in the next blog entry
  • it allows to define class level constraints

Class level constraints are regular constraints (annotation / implementation duo) which apply on a class rather than a property. Said differently, class-level constraints receive the object instance (rather than the property value) in isValid.

@Address 
public class Address {
    @NotNull @Max(50) private String street1;
    @Max(50) private String street2;
    @Max(10) @NotNull private String zipCode;
    @Max(20) @NotNull String city;
    @NotNull private Country country;
    
    ...
}
@ConstraintValidator(MultiCountryAddressValidator.class)
@Target(TYPE)
@Retention(RUNTIME)
public @interface Address {
    String message() default "{error.address}";
    String[] groups() default {};
}
public class MultiCountryAddressValidator implements Constraint<Address> {
        public void initialize(Address constraintAnnotation) {
                //initialize the zipcode/city/country correlation service
        }

        /**
         * Validate zipcode and city depending on the country
         */
        public boolean isValid(Object object) {
                if ( ! (object instanceof Address) )
                        throw new IllegalArgumentException("@Address only applies to Address");
                Address address = (Address) object;
                Country country = address.getCountry();
                if ( country.getISO2() == "FR" ) {
                    //check address.getZipCode() structure for France (5 numbers)
                    //check zipcode and city correlation (calling an external service?)
                    return isValid;
                }
                else if ( country.getISO2() == "GR" ) {
                    //check address.getZipCode() structure for Greece
                    //no zipcode / city correlation available at the moment
                    return isValid;
                }
                ...
        }
}

The advanced address validation rules have been left out of the address object and implemented by MultiCountryAddressValidator. By accessing the object instance, class level constraints have a lot of flexibility and can validate multiple correlated properties. Note that ordering is left out of the equation here, we will come back to it in the next post.

The expert group has discussed various multiple properties support approaches: we think the class level constraint approach provides both enough simplicity and flexibility compared to other property level approaches involving dependencies. Your feedback is welcome.

Conclusion

Custom constraints are at the heart of JSR 303 Bean Validation flexibility. It should not be considered awkward to write a custom constraint:

  • the validation routine captures the exact validation semantic you expect
  • a carefully chosen annotation name will make constraints extremely readable in the code

Please let us know what you think here. You can download the full specification draft there. The next blog entry will cover groups, constraints subsets and validation ordering.


Back to top