Hibernate.orgCommunity Documentation

Chapter 3. Creating custom constraints

3.1. Creating a simple constraint
3.1.1. The constraint annotation
3.1.2. The constraint validator
3.1.3. The error message
3.1.4. Using the constraint
3.2. Constraint composition

Though the Bean Validation API defines a whole set of standard constraint annotations one can easily think of situations in which these standard annotations won't suffice. For these cases you are able to create custom constraints tailored to your specific validation requirements in a simple manner.

To create a custom constraint, the following three steps are required:

Let's write a constraint annotation, that can be used to express that a given string shall either be upper case or lower case. We'll apply it later on to the licensePlate field of the Car class from Chapter 1, Getting started to ensure, that the field is always an upper-case string.

First we need a way to express the two case modes. We might use String constants, but a better way to go is to use a Java 5 enum for that purpose:

Now we can define the actual constraint annotation. If you've never designed an annotation before, this may look a bit scary, but actually it's not that hard:

An annotation type is defined using the @interface keyword. All attributes of an annotation type are declared in a method-like manner. The specification of the Bean Validation API demands, that any constraint annotation defines

Besides those three mandatory attributes (messge, groups and payload) we add another one allowing for the required case mode to be specified. The name value is a special one, which can be omitted upon using the annotation, if it is the only attribute specified, as e.g. in @CheckCase(CaseMode.UPPER).

In addition we annotate the annotation type with a couple of so-called meta annotations:

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): Says, that methods, fields and annotation declarations may be annotated with @CheckCase (but not type declarations e.g.)

  • @Retention(RUNTIME): Specifies, that annotations of this type will be available at runtime by the means of reflection

  • @Constraint(validatedBy = CheckCaseValidator.class): Specifies the validator to be used to validate elements annotated with @CheckCase

  • @Documented: Says, that the use of @CheckCase will be contained in the JavaDoc of elements annotated with it

Next, we need to implement a constraint validator, that's able to validate elements with a @CheckCase annotation. To do so, we implement the interface ConstraintValidator as shown below:

The ConstraintValidator interface defines two type parameters, which we set in our implementation. The first one specifies the annotation type to be validated (in our example CheckCase), the second one the type of elements, which the validator can handle (here String).

In case a constraint annotation is allowed at elements of different types, a ConstraintValidator for each allowed type has to be implemented and registered at the constraint annotation as shown above.

The implementation of the validator is straightforward. The initialize() method gives us access to the attribute values of the annotation to be validated. In the example we store the CaseMode in a field of the validator for further usage.

In the isValid() method we implement the logic, that determines, whether a String is valid according to a given @CheckCase annotation or not. This decision depends on the case mode retrieved in initialize(). As the Bean Validation specification recommends, we consider null values as being valid. If null is not a valid value for an element, it should be annotated with @NotNull explicitely.

The passed-in ConstraintValidatorContext could be used to raise any custom validation errors, but as we are fine with the default behavior, we can ignore that parameter for now.

Now that our first custom constraint is completed, we can use it in the Car class from the Chapter 1, Getting started chapter to specify that the licensePlate field shall only contain upper-case strings:

Finally let's demonstrate in a little test that the @CheckCase constraint is properly validated:

Looking at the licensePlate field of the Car class in Example 3.5, “Applying the CheckCase constraint”, we see three constraint annotations already. In complexer scenarios, where even more constraints could be applied to one element, this might become a bit confusing easily. Furthermore, if we had a licensePlate field in another class, we would have to copy all constraint declarations to the other class as well, violating the DRY principle.

This problem can be tackled using compound constraints. In the following we create a new constraint annotation @ValidLicensePlate, that comprises the constraints @NotNull, @Size and @CheckCase:

To do so, we just have to annotate the constraint declaration with its comprising constraints (btw. that's exactly why we allowed annotation types as target for the @CheckCase annotation). As no additional validation is required for the @ValidLicensePlate annotation itself, we don't declare a validator within the @Constraint meta annotation.

Using the new compound constraint at the licensePlate field now is fully equivalent to the previous version, where we declared the three constraints directly at the field itself:

The set of ConstraintViolations retrieved when validating a Car instance will contain an entry for each violated composing constraint of the @ValidLicensePlate constraint. If you rather prefer a single ConstraintViolation in case any of the composing constraints is violated, the @ReportAsSingleViolation meta constraint can be used as follows: