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:
Create a constraint annotation
Implement a validator
Define a default error message
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
an attribute message that returns the default key for creating error messages in case the constraint is violated
an attribute 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 behaviour 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.3, “Class-level
constraints”), the element type
TYPE
would have to be used. Constraints
targetting 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
object 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.3, “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 { };
}
Copyright © 2009 - 2013 Red Hat, Inc. & Gunnar Morling