Bean Validation: Advanced Topics
This chapter describes how to create custom constraints, custom validator messages, and constraint groups using the Jakarta Bean Validation (Bean Validation).
Creating Custom Constraints
Jakarta Bean Validation defines annotations, interfaces, and classes to allow developers to create custom constraints.
Using the Built-In Constraints to Make a New Constraint
Jakarta Bean Validation includes several built-in constraints that can be combined to create new, reusable constraints. This can simplify constraint definition by allowing developers to define a custom constraint made up of several built-in constraints that may then be applied to component attributes with a single annotation.
@Pattern.List({
/* A number of format “+1-NNN-NNN-NNNN” */
@Pattern(regexp = “\\+1-\\d{3}-\\d{3}-\\d{4})
})
@Constraint(validatedBy = {})
@Documented
@Target({ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER
ElementType.Type_Use})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(List.class)
public @interface USPhoneNumber {
String message() default "Not a valid US Phone Number";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
@Target({ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER
ElementType.Type_Use })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@interface List {
USPhoneNumber[] value();
}
}
You can also implement a Constraint Validator
to validate the constraint @USPhoneNumber
.
For more information about using Constraint Validator
, see jakarta.validation.ConstraintValidator
.
@USPhoneNumber
protected String phone;
Removing Ambiguity in Constraint Targets
Custom constraints that can be applied to both return values and method parameters require a validationAppliesTo
element to identify the target of the constraint.
@Constraint(validatedBy=MyConstraintValidator.class)
@Target({ METHOD, FIELD, TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface MyConstraint {
String message() default "{com.example.constraint.MyConstraint.message}";
Class<?>[] groups() default {};
ConstraintTarget validationAppliesTo() default ConstraintTarget.PARAMETERS;
...
}
This constraint sets the validationAppliesTo
target by default to the method parameters.
@MyConstraint(validationAppliesTo=ConstraintTarget.RETURN_TYPE)
public String doSomething(String param1, String param2) { ... }
In the preceding example, the target is set to the return value of the method.
Implementing Temporal Constraints Using ClockProvider
From Bean Validation 2.0 onwards, a Clock
instance is available for validator implementations to validate any temporal date or time based constraints.
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
ClockProvider clockProvider = validatorFactory.getClockProvider();
java.time.Clock Clock = clockProvider.getClock();
You can also register a custom ClockProvider
with a ValidatorFactory
:
//Register a custom clock provider implementation with validator factory
ValidatorFactory factory = Validation
.byDefaultProvider().configure()
.clockProvider( new CustomClockProvider() )
.buildValidatorFactory();
//Retrieve and use the custom Clock Provider and Clock in the Validator implementation
public class CustomConstraintValidator implements ConstraintValidator<CustomConstraint, Object> {
public boolean isValid(Object value, ConstraintValidatorContext context){
java.time.Clock clock = context.getClockProvider().getClock();
...
...
}
}
See ClockProvider in https://jakarta.ee/specifications/platform/9/apidocs/.
Custom Constraints
Consider an employee in a firm located in U.S.A. When you register the phone number of an employee or modify the phone number, the phone number needs to be validated to ensure that the phone number conforms to US phone number pattern.
public class Employee extends Person {
@USPhoneNumber
protected String phone;
public Employee(String name, String phone, int age){
super(name, age);
this.phone = phone;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
The constraint definition @USPhoneNumber
is define in the sample listed under Using the Built-In Constraints to Make a New Constraint.
In the sample, another constraint @Pattern
is used to validate the phone number.
Using In-Built Value Extractors in Custom Containers
- Cascading validation
-
Bean Validation supports cascading validation for various entities. You can specify
@Valid
on a member of the object that is validated to ensure that the member is also validated in a cascading fashion. You can validate type arguments, for example, parameterized types and its members if the members have the specified@Valid
annotation.
public class Department {
private List<@Valid Employee> employeesList;
}
By specifying @Valid
on a parameterized type, when an instance of Department
is validated, all elements such as Employee
in the employeesList
are also validated.
In this example, each employee’s "phone" is validated against the constraint @USPhoneNumber
.
For more information see https://jakarta.ee/specifications/platform/9/apidocs/
- Value Extractor
-
While validating the object or the object graph, it may be necessary to validate the constraints in the parameterized types of a container as well. To validate the elements of the container, the validator must extract the values of these elements in the container. For example, in order to validate the element values of
List
against one or more constraints such asList<@NotOnVacation Employee>
or to apply cascading validation toList<@Valid Employee>
, you need a value extractor for the containerList
.
Jakarta Bean validation provides in-built value extractors for most commonly used container types such as List, Iterable, and others. However, it is also possible to implement and register value-extractor implementations for custom container types or override the in-built value-extractor implementations.
Consider a Statistics Calculator for a group of Person
entity and Employee
is one of the sub-type of the entity Person
.
public class StatsCalculator<T extends Person> {
/* Cascading validation as well as @NotNull constraint */
private List<@NotNull @Valid T> members = new ArrayList<T>();
public void addMember(T member) {
members.add(member);
}
public boolean removeMember(T member) {
return members.remove(member);
}
public int getAverageAge() {
if (members.size() == 0)
return 0;
short sum = 0;
for (T member : members) {
if(member != null) {
sum += member.getAge();
}
}
return sum / members.size();
}
public int getOldest() {
int oldest = -1;
for (T member : members) {
if(member != null) {
if (member.getAge() > oldest) {
oldest = member.getAge();
}
}
}
return oldest;
}
}
When the StatsCalculator
is validated, the "members" field is also validated.
The in-built value extractor for List
is used to extract the values of List
to validate the elements in List
.
In the case of an employee based List, each "Employee" element is validated.
For example, an employee’s "phone" is validated using the @USPhoneNumber
constraint.
In the following example, let us consider a StatisticsPrinter
that prints the statistics or displays the statistics on screen.
public class StatisticsPrinter {
private StatsCalculator<@Valid Employee> calculator;
public StatisticsPrinter(StatsCalculator<Employee> statsCalculator){
this.calculator = statsCalculator;
}
public void displayStatistics(){
//Use StatsCalculator, get stats, format and display them.
}
public void printStatistics(){
//Use StatsCalculator, get stats, format and print them.
}
}
The container StatisticsPrinter
uses StatisticsCalculator
.
When StatisticsPrinter
is validated, the StatisticsCalculator
is also validated by using the cascading validation such as @Valid
annotation.
However, in order to retrieve the values of StatsCalculator
container type, a value extractor is required.
An implementation of ValueExtractor
for StatsCalculator
is as follows:
public class ExtractorForStatsCalculator implements ValueExtractor<StatsCalculator<@ExtractedValue ?>> {
@Override
public void extractValues(StatsCalculator<@ExtractedValue ?> statsCalculator,
ValueReceiver valueReceiver) {
/* Simple value retrieval is done here.
It is possible to adapt or unwrap the value if required.*/
valueReceiver.value("<extracted value>", statsCalculator);
}
}
There are multiple mechanisms to register the ValueExtractor
with Jakarta Bean Validation.
See, “Registering ValueExtractor” implementations section in the Jakarta Bean Validation specification https://jakarta.ee/specifications/bean-validation/3.0/.
One of the mechanisms is to register the value extractor with Jakarta Bean Validation Context.
ValidatorFactory validatorFactory = Validation
.buildDefaultValidatorFactory();
ValidatorContext context = validatorFactory.
usingContext()
.addValueExtractor(new ExtractorForStatsCalculator());
Validator validator = context.getValidator();
Using this validator, StatsisticsPrinter
is validated in the following sequence of operations:
-
StatisticsPrinter
is validated.-
The members of
StatisticsPrinter
that need cascading validation are validated. -
For container types, value extractor is determined. In the case of
StatsCalculator
,ExtractorForStatsCalculator
is found and then values are retrieved for validation. -
StatsCalculator
and its members such asList
are validated. -
In-built
ValueExtractor
forjava.util.List
is used to retrieve the values of elements of the list and the validated. In this case, Employee and the field "phone" that is annotated with@USPhoneNumber
constraint is validated.
-
Customizing Validator Messages
Jakarta Bean Validation includes a resource bundle of default messages for the built-in constraints. These messages can be customized and can be localized for non-English-speaking locales.
The ValidationMessages Resource Bundle
The ValidationMessages
resource bundle and the locale variants of this resource bundle contain strings that override the default validation messages.
The ValidationMessages
resource bundle is typically a properties file, ValidationMessages.properties
, in the default package of an application.
Grouping Constraints
Constraints may be added to one or more groups.
Constraint groups are used to create subsets of constraints so that only certain constraints will be validated for a particular object.
By default, all constraints are included in the Default
constraint group.
Constraint groups are represented by interfaces.
public interface Employee {}
public interface Contractor {}
Constraint groups can inherit from other groups.
public interface Manager extends Employee {}
When a constraint is added to an element, the constraint declares the groups to which that constraint belongs by specifying the class name of the group interface name in the groups
element of the constraint.
@NotNull(groups=Employee.class)
Phone workPhone;
Multiple groups can be declared by surrounding the groups with braces ({
and }
) and separating the groups' class names with commas.
@NotNull(groups={ Employee.class, Contractor.class })
Phone workPhone;
If a group inherits from another group, validating that group results in validating all constraints declared as part of the supergroup.
For example, validating the Manager
group results in the workPhone
field being validated, because Employee
is a superinterface of Manager
.
Customizing Group Validation Order
By default, constraint groups are validated in no particular order. There are cases in which some groups should be validated before others. For example, in a particular class, basic data should be validated before more advanced data.
To set the validation order for a group, add a jakarta.validation.GroupSequence
annotation to the interface definition, listing the order in which the validation should occur.
@GroupSequence({Default.class, ExpensiveValidationGroup.class})
public interface FullValidationGroup {}
When validating FullValidationGroup
, first the Default
group is validated.
If all the data passes validation, then the ExpensiveValidationGroup
group is validated.
If a constraint is part of both the Default
and the ExpensiveValidationGroup
groups, the constraint is validated as part of the Default
group and will not be validated on the subsequent ExpensiveValidationGroup
pass.
Using Method Constraints in Type Hierarchies
If you add validation constraints to objects in an inheritance hierarchy, take special care to avoid unintended errors when using subtypes.
For a given type, subtypes should be able to be substituted without encountering errors.
For example, if you have a Person
class and an Employee
subclass that extends Person
, you should be able to use Employee
instances wherever you might use Person
instances.
If Employee
overrides a method in Person
by adding method parameter constraints, code that works correctly with Person
objects may throw validation exceptions with Employee
objects.
The following code shows an incorrect use of method parameter constraints within a class hierarchy:
public class Person {
...
public void setPhone(String phone) { ... }
}
public class Employee extends Person {
...
@Override
public void setPhone(@Verified String phone) { ... }
}
By adding the @Verified
constraint to Employee.setPhone
, parameters that were valid with Person.setPhone
will not be valid with Employee.setPhone
.
This is called strengthening the preconditions (that is, the method parameters) of a subtype’s method.
You may not strengthen the preconditions of subtype method calls.
Similarly, the return values from method calls should not be weakened in subtypes. The following code shows an incorrect use of constraints on method return values in a class hierarchy:
public class Person {
...
@Verified
public USPhoneNumber getPhone() { ... }
}
public class Employee extends Person {
...
@Override
public USPhoneNumber getPhone() { ... }
}
In this example, the Employee.getPhone
method removes the @Verified
constraint on the return value.
Return values that would be not pass validation when calling Person.getEmail
are allowed when calling Employee.getPhone
.
This is called weakening the postconditions (that is, return values) of a subtype.
You may not weaken the postconditions of a subtype method call.
If your type hierarchy strengthens the preconditions or weakens the postconditions of subtype method calls, a jakarta.validation.ConstraintDeclarationException
will be thrown by the Jakarta Bean Validation runtime.
Classes that implement several interfaces that each have the same method signature, known as parallel types, need to be aware of the constraints applied to the interfaces that they implement to avoid strengthening the preconditions. For example:
public interface PaymentService {
void processOrder(Order order, double amount);
...
}
public interface CreditCardPaymentService {
void processOrder(@NotNull Order order, @NotNull double amount);
...
}
public class MyPaymentService implements PaymentService,
CreditCardPaymentService {
@Override
public void processOrder(Order order, double amount) { ... }
...
}
In this case, MyPaymentService
has the constraints from the processOrder
method in CreditCardPaymentService
, but client code that calls PaymentService.processOrder
doesn’t expect these constraints.
This is another example of strengthening the preconditions of a subtype and will result in a ConstraintDeclarationException
.
Rules for Using Method Constraints in Type Hierarchies
The following rules define how method validation constraints should be used in type hierarchies.
-
Do not add method parameter constraints to overridden or implemented methods in a subtype.
-
Do not add method parameter constraints to overridden or implemented methods in a subtype that was originally declared in several parallel types.
-
You may add return value constraints to an overridden or implemented method in a subtype.