Hibernate.orgCommunity Documentation
The Bean Validation API defines a whole set of standard constraint annotations such as @NotNull
,
@Size
etc. In cases where these buit-in constraints are not sufficient, you cean easily create
custom constraints tailored to your specific validation requirements.
To create a custom constraint, the following three steps are required:
This section shows how to write a constraint annotation which can be used to ensure that a given
string is either completely upper case or lower case. Later on this constraint will be applied to
the licensePlate
field of the Car
class from Chapter 1, Getting started to ensure, that
the field is always an upper-case string.
The first thing needed is a way to express the two case modes. While you could use String
constants,
a better approach is using a Java 5 enum for that purpose:
Example 6.1. Enum CaseMode
to express upper vs. lower case
package org.hibernate.validator.referenceguide.chapter06; public enum CaseMode { UPPER, LOWER; }
The next step is to 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:
Example 6.2. Defining the @CheckCase
constraint annotation
package org.hibernate.validator.referenceguide.chapter06; @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = CheckCaseValidator.class) @Documented public @interface CheckCase { String message() default "{org.hibernate.validator.referenceguide.chapter06.CheckCase." + "message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; CaseMode value(); @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented @interface List { CheckCase[] value(); } }
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
message
that returns the default key for creating error messages in case the
constraint is violated
groups
that allows the specification of validation groups, to which this constraint
belongs (see Chapter 5, Grouping constraints). This must default to an empty array of type Class<?>.
an attribute payload
that can be used by clients of the Bean Validation API to assign custom
payload objects to a constraint. This attribute is not used by the API itself. An example for a
custom payload could be the definition of a severity:
public class Severity { public interface Info extends Payload { } public interface Error extends Payload { } } public class ContactDetails { @NotNull(message = "Name is mandatory", payload = Severity.Error.class) private String name; @NotNull(message = "Phone number not specified, but not mandatory", payload = Severity.Info.class) private String phoneNumber; // ... }
Now a client can after the validation of a ContactDetails
instance access the severity of a
constraint using ConstraintViolation.getConstraintDescriptor().getPayload()
and adjust its behavior
depending on the severity.
Besides these three mandatory attributes there is another one, value
, allowing for the required case
mode to be specified. The name value
is a special one, which can be omitted when using the
annotation, if it is the only attribute specified, as e.g. in @CheckCase(CaseMode.UPPER)
.
In addition, the constraint annotation is decorated with a couple of meta annotations:
@Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
: Defines the supported target element types
for the constraint. @CheckCase
may be used on fields (element type FIELD
), JavaBeans properties as
well as method return values (METHOD
) and method/constructor parameters (PARAMETER
). The element
type ANNOTATION_TYPE
allows for the creation of composed constraints
(see Section 6.4, “Constraint composition”) based on @CheckCase
.
When creating a class-level constraint (see Section 2.1.4, “Class-level constraints”), the element
type TYPE
would have to be used. Constraints targeting the return value of a constructor need to
support the element type CONSTRUCTOR
. Cross-parameter constraints (see
Section 6.3, “Cross-parameter constraints”) which are used to validate all the parameters of a method
or constructor together, must support METHOD
or CONSTRUCTOR
, respectively.
@Retention(RUNTIME)
: Specifies, that annotations of this type will be available at runtime by the
means of reflection
@Constraint(validatedBy = CheckCaseValidator.class)
: Marks the annotation type as constraint
annotation and specifies the validator to be used to validate elements annotated with @CheckCase
.
If a constraint may be used on several data types, several validators may be specified, one for
each data type.
@Documented
: Says, that the use of @CheckCase
will be contained in the JavaDoc of elements
annotated with it
Finally, there is an inner annotation type named List
. This annotation allows to specify several
@CheckCase
annotations on the same element, e.g. with different validation groups and messages.
While also another name could be used, the Bean Validation specification recommends to use the name
List
and make the annotation an inner annotation of the corresponding constraint type.
Having defined the annotation, you need to create a constraint validator, which is able to validate
elements with a @CheckCase
annotation. To do so, implement the interface ConstraintValidator
as
shown below:
Example 6.3. Implementing a constraint validator for the constraint @CheckCase
package org.hibernate.validator.referenceguide.chapter06; public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> { private CaseMode caseMode; @Override public void initialize(CheckCase constraintAnnotation) { this.caseMode = constraintAnnotation.value(); } @Override public boolean isValid(String object, ConstraintValidatorContext constraintContext) { if ( object == null ) { return true; } if ( caseMode == CaseMode.UPPER ) { return object.equals( object.toUpperCase() ); } else { return object.equals( object.toLowerCase() ); } } }
The ConstraintValidator
interface defines two type parameters which are set in the implementation.
The first one specifies the annotation type to be validated (CheckCase
), the second one the type of
elements, which the validator can handle (String
). In case a constraint supports several data 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 you access to
the attribute values of the validated constraint and allows you to store them in a field of the
validator as shown in the example.
The isValid()
method contains the actual validation logic. For @CheckCase
this is the check whether
a given string is either completely lower case or upper case, depending on the case mode retrieved
in initialize()
. Note that the Bean Validation specification recommends to consider null values as
being valid. If null
is not a valid value for an element, it should be annotated with @NotNull
explicitly.
Example 6.3, “Implementing a constraint validator for the constraint @CheckCase
”
relies on the default error message generation by just returning true
or false
from the isValid()
method. Using the passed ConstraintValidatorContext
object it is possible to either add additional
error messages or completely disable the default error message generation and solely define custom
error messages. The ConstraintValidatorContext
API is modeled as fluent interface and is best
demonstrated with an example:
Example 6.4. Using ConstraintValidatorContext
to define custom error messages
package org.hibernate.validator.referenceguide.chapter06.constraintvalidatorcontext; public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> { private CaseMode caseMode; @Override public void initialize(CheckCase constraintAnnotation) { this.caseMode = constraintAnnotation.value(); } @Override public boolean isValid(String object, ConstraintValidatorContext constraintContext) { if ( object == null ) { return true; } boolean isValid; if ( caseMode == CaseMode.UPPER ) { isValid = object.equals( object.toUpperCase() ); } else { isValid = object.equals( object.toLowerCase() ); } if ( !isValid ) { constraintContext.disableDefaultConstraintViolation(); constraintContext.buildConstraintViolationWithTemplate( "{org.hibernate.validator.referenceguide.chapter03." + "constraintvalidatorcontext.CheckCase.message}" ) .addConstraintViolation(); } return isValid; } }
Example 6.4, “Using ConstraintValidatorContext
to define custom error messages”
shows how you can disable the default error message generation and add a custom error message using
a specified message template. In this example the use of the ConstraintValidatorContext
results in
the same error message as the default error message generation.
It is important to add each configured constraint violation by calling addConstraintViolation()
.
Only after that the new constraint violation will be created.
Refer to Section 6.2.1, “Custom property paths” to learn how to use the ConstraintValidatorContext
API to
control the property path of constraint violations for class-level constraints.
The last missing building block is an error message which should be used in case a @CheckCase
constraint is violated. To define this, create a file ValidationMessages.properties with the
following contents (see also Section 4.1, “Default message interpolation”):
Example 6.5. Defining a custom error message for the CheckCase
constraint
org.hibernate.validator.referenceguide.chapter06.CheckCase.message=Case mode must be {value}.
If a validation error occurs, the validation runtime will use the default value, that you specified
for the message attribute of the @CheckCase
annotation to look up the error message in this resource
bundle.
You can now use the constraint in the Car
class from the Chapter 1, Getting started chapter to
specify that the licensePlate
field should only contain upper-case strings:
Example 6.6. Applying the @CheckCase
constraint
package org.hibernate.validator.referenceguide.chapter06; public class Car { @NotNull private String manufacturer; @NotNull @Size(min = 2, max = 14) @CheckCase(CaseMode.UPPER) private String licensePlate; @Min(2) private int seatCount; public Car ( String manufacturer, String licencePlate, int seatCount ) { this.manufacturer = manufacturer; this.licensePlate = licencePlate; this.seatCount = seatCount; } //getters and setters ... }
Finally, Example 6.7, “Validating objects with the @CheckCase
constraint” demonstrates how validating a Car
instance with an invalid
license plate causes the @CheckCase
constraint to be violated.
Example 6.7. Validating objects with the @CheckCase
constraint
//invalid license plate Car car = new Car( "Morris", "dd-ab-123", 4 ); Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car ); assertEquals( 1, constraintViolations.size() ); assertEquals( "Case mode must be UPPER.", constraintViolations.iterator().next().getMessage() ); //valid license plate car = new Car( "Morris", "DD-AB-123", 4 ); constraintViolations = validator.validate( car ); assertEquals( 0, constraintViolations.size() );
As discussed earlier, constraints can also be applied on the class level to validate the state of an
entire object. Class-level constraints are defined in the same was as are property constraints.
Example 6.8, “Implementing a class-level constraint” shows constraint annotation and validator of the
@ValidPassengerCount
constraint you already saw in use in Example 2.7, “Class-level constraint”.
Example 6.8. Implementing a class-level constraint
package org.hibernate.validator.referenceguide.chapter06.classlevel; @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = { ValidPassengerCountValidator.class }) @Documented public @interface ValidPassengerCount { String message() default "{org.hibernate.validator.referenceguide.chapter06.classlevel." + "ValidPassengerCount.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
package org.hibernate.validator.referenceguide.chapter06.classlevel; public class ValidPassengerCountValidator implements ConstraintValidator<ValidPassengerCount, Car> { @Override public void initialize(ValidPassengerCount constraintAnnotation) { } @Override public boolean isValid(Car car, ConstraintValidatorContext context) { if ( car == null ) { return true; } return car.getPassengers().size() <= car.getSeatCount(); } }
As the example demonstrates, you need to use the element type TYPE
in the @Target
annotation. This
allows the constraint to be put on type definitions. The validator of the constraint in the example
receives a Car
in the isValid()
method and can access the complete object state to decide whether
the given instance is valid or not.
By default the constraint violation for a class-level constraint is reported on the level of the
annotated type, e.g. Car
.
In some cases it is preferable though that the violation’s property path refers to one of the
involved properties. For instance you might want to report the @ValidPassengerCount
constraint
against the passengers property instead of the Car
bean.
Example 6.9, “Adding a new ConstraintViolation
with custom property path”
shows how this can be done by using the constraint validator context passed to isValid()
to build a
custom constraint violation with a property node for the property passengers. Note that you also
could add several property nodes, pointing to a sub-entity of the validated bean.
Example 6.9. Adding a new ConstraintViolation
with custom property path
package org.hibernate.validator.referenceguide.chapter06.custompath; public class ValidPassengerCountValidator implements ConstraintValidator<ValidPassengerCount, Car> { @Override public void initialize(ValidPassengerCount constraintAnnotation) { } @Override public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) { if ( car == null ) { return true; } boolean isValid = car.getPassengers().size() <= car.getSeatCount(); if ( !isValid ) { constraintValidatorContext.disableDefaultConstraintViolation(); constraintValidatorContext .buildConstraintViolationWithTemplate( "{my.custom.template}" ) .addPropertyNode( "passengers" ).addConstraintViolation(); } return isValid; } }
Bean Validation distinguishes between two different kinds of constraints.
Generic constraints (which have been discussed so far) apply to the annotated element, e.g. a type, field, method parameter or return value etc. Cross-parameter constraints, in contrast, apply to the array of parameters of a method or constructor and can be used to express validation logic which depends on several parameter values.
In order to define a cross-parameter constraint, its validator class must be annotated with
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
. The type parameter T
from the
ConstraintValidator
interface must resolve to either Object
or Object[]
in order to receive the
array of method/constructor arguments in the isValid()
method.
The following example shows the definition of a cross-parameter constraint which can be used to
check that two Date
parameters of a method are in the correct order:
Example 6.10. Cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter; @Constraint(validatedBy = ConsistentDateParameterValidator.class) @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented public @interface ConsistentDateParameters { String message() default "{org.hibernate.validator.referenceguide.chapter06." + "crossparameter.ConsistentDateParameters.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
The definition of a cross-parameter constraint isn’t any different from defining a generic
constraint, i.e. it must specify the members message()
, groups()
and payload()
and be annotated with
@Constraint
. This meta annotation also specifies the corresponding validator, which is shown in
Example 6.11, “Generic and cross-parameter constraint”. Note that besides the element types METHOD
and CONSTRUCTOR
also ANNOTATION_TYPE
is specified as target of the annotation, in order to enable the creation of
composed constraints based on @ConsistentDateParameters
(see
Section 6.4, “Constraint composition”).
Cross-parameter constraints are specified directly on the declaration of a method or constructor,
which is also the case for return value constraints. In order to improve code readability, it is
therefore recommended to chose constraint names - such as @ConsistentDateParameters
- which make the
constraint target apparent.
Example 6.11. Generic and cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter; @SupportedValidationTarget(ValidationTarget.PARAMETERS) public class ConsistentDateParameterValidator implements ConstraintValidator<ConsistentDateParameters, Object[]> { @Override public void initialize(ConsistentDateParameters constraintAnnotation) { } @Override public boolean isValid(Object[] value, ConstraintValidatorContext context) { if ( value.length != 2 ) { throw new IllegalArgumentException( "Illegal method signature" ); } //leave null-checking to @NotNull on individual parameters if ( value[0] == null || value[1] == null ) { return true; } if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) { throw new IllegalArgumentException( "Illegal method signature, expected two " + "parameters of type Date." ); } return ( (Date) value[0] ).before( (Date) value[1] ); } }
As discussed above, the validation target PARAMETERS
must be configured for a cross-parameter
validator by using the @SupportedValidationTarget
annotation. Since a cross-parameter constraint
could be applied to any method or constructor, it is considered a best practice to check for the
expected number and types of parameters in the validator implementation.
As with generic constraints, null
parameters should be considered valid and @NotNull
on the
individual parameters should be used to make sure that parameters are not null
.
Similar to class-level constraints, you can create custom constraint violations on single parameters
instead of all parameters when validating a cross-parameter constraint. Just obtain a node builder
from the ConstraintValidatorContext
passed to isValid()
and add a parameter node by calling
addParameterNode()
. In the example you could use this to create a constraint violation on the end
date parameter of the validated method.
In rare situations a constraint is both, generic and cross-parameter. This is the case if a
constraint has a validator class which is annotated with
@SupportedValidationTarget({ValidationTarget.PARAMETERS, ValidationTarget.ANNOTATED_ELEMENT})
or if
it has a generic and a cross-parameter validator class.
When declaring such a constraint on a method which has parameters and also a return value, the
intended constraint target can’t be determined. Constraints which are generic and cross-parameter at
the same time, must therefore define a member validationAppliesTo()
which allows the constraint user
to specify the constraint’s target as shown in Example 6.12, “Generic and cross-parameter constraint”.
Example 6.12. Generic and cross-parameter constraint
package org.hibernate.validator.referenceguide.chapter06.crossparameter; @Constraint(validatedBy = { ScriptAssertObjectValidator.class, ScriptAssertParametersValidator.class }) @Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE }) @Retention(RUNTIME) @Documented public @interface ScriptAssert { String message() default "{org.hibernate.validator.referenceguide.chapter06." + "crossparameter.ScriptAssert.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; String script(); ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT; }
The @ScriptAssert
constraint has two validators (not shown), a generic and a cross-parameter one and
thus defines the member validationAppliesTo()
. The default value IMPLICIT
allows to derive the
target automatically in situations where this is possible (e.g. if the constraint is declared on a
field or on a method which has parameters but no return value).
If the target can not be determined implicitly, it must be set by the user to either PARAMETERS
or
RETURN_VALUE
as shown in Example 6.13, “Specifying the target for a generic and cross-parameter constraint”.
Example 6.13. Specifying the target for a generic and cross-parameter constraint
@ScriptAssert(script = "arg1.size() <= arg0", validationAppliesTo = ConstraintTarget.PARAMETERS) public Car buildCar(int seatCount, List<Passenger> passengers) { //... }
Looking at the licensePlate
field of the Car
class in Example 6.6, “Applying the @CheckCase
constraint”, you 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 there was a licensePlate
field in another class, you would have to copy all constraint declarations to the other class as
well, violating the DRY principle.
You can address this kind of problem by creating higher level constraints, composed from several
basic constraints. Example 6.14, “Creating a composing constraint @ValidLicensePlate
” shows a composed constraint annotation which
comprises the constraints @NotNull
, @Size
and @CheckCase
:
Example 6.14. Creating a composing constraint @ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition; @NotNull @Size(min = 2, max = 14) @CheckCase(CaseMode.UPPER) @Target({ METHOD, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = { }) @Documented public @interface ValidLicensePlate { String message() default "{org.hibernate.validator.referenceguide.chapter06." + "constraintcomposition.ValidLicensePlate.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }
To create a composed constraint, simply annotate the constraint declaration with its comprising
constraints. If the composed constraint itself requires a validator, this validator is to be
specified within the @Constraint
annotation. For composed constraints which don’t need an additional
validator such as @ValidLicensePlate
, just set validatedBy()
to an empty array.
Using the new composed constraint at the licensePlate
field is fully equivalent to the previous
version, where the three constraints were declared directly at the field itself:
Example 6.15. Application of composing constraint ValidLicensePlate
package org.hibernate.validator.referenceguide.chapter06.constraintcomposition; public class Car { @ValidLicensePlate private String licensePlate; //... }
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:
Example 6.16. Using @ReportAsSingleViolation
//... @ReportAsSingleViolation public @interface ValidLicensePlate { String message() default "{org.hibernate.validator.referenceguide.chapter06." + "constraintcomposition.ValidLicensePlate.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; }