1. Overview
In this article, we’ll discuss how to define and validate method constraints using Jakarta Bean Validation 3.0
In the previous article, we discussed JSR-380 with its built-in annotations, and how to implement property validation.
Here, we’ll focus on the different types of method constraints such as:
- single-parameter constraints
- cross-parameter
- return constraints
Also, we’ll have a look at how to validate the constraints manually and automatically using Spring Validator.
For the following examples, we need exactly the same dependencies as in Java Bean Validation Basics.
2. Declaration of Method Constraints
To get started, we’ll first discuss how to declare constraints on method parameters and return values of methods.
As mentioned before, we can use annotations from jakarta.validation.constraints, but we can also specify custom constraints (e. g. for custom constraints or cross-parameter constraints).
2.1. Single Parameter Constraints
Defining constraints on single parameters is straightforward. We simply have to add annotations to each parameter as required:
public void createReservation(@NotNull @Future LocalDate begin,
@Min(1) int duration, @NotNull Customer customer) {
// ...
}
Likewise, we can use the same approach for constructors:
public class Customer {
public Customer(@Size(min = 5, max = 200) @NotNull String firstName,
@Size(min = 5, max = 200) @NotNull String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
// properties, getters, and setters
}
2.2. Using Cross-Parameter Constraints
In some cases, we might need to validate multiple values at once, e.g., two numeric amounts being one bigger than the other.
For these scenarios, we can define custom cross-parameter constraints, which might depend on two or more parameters.
Cross-parameter constraints can be considered as the method validation equivalent to class-level constraints. We could use both to implement validation based on several properties.
Let’s think about a simple example: a variation of the createReservation() method from the previous section takes two parameters of type LocalDate: a begin date and an end date.
Consequently, we want to make sure that begin is in the future, and end is after begin. Unlike in the previous example, we can’t define this using single parameter constraints.
Instead, we need a cross-parameter constraint.
In contrast to single-parameter constraints, cross-parameter constraints are declared on the method or constructor:
@ConsistentDateParameters
public void createReservation(LocalDate begin,
LocalDate end, Customer customer) {
// ...
}
2.3. Creating Cross-Parameter Constraints
To implement the @ConsistentDateParameters constraint, we need two steps.
First, we need to define the constraint annotation:
@Constraint(validatedBy = ConsistentDateParameterValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ConsistentDateParameters {
String message() default
"End date must be after begin date and both must be in the future";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Here, these three properties are mandatory for constraint annotations:
- message – returns the default key for creating error messages, this enables us to use message interpolation
- groups – allows us to specify validation groups for our constraints
- payload – can be used by clients of the Bean Validation API to assign custom payload objects to a constraint
For details how to define a custom constraint, have a look at the official documentation.
After that, we can define the validator class:
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class ConsistentDateParameterValidator
implements ConstraintValidator<ConsistentDateParameters, Object[]> {
@Override
public boolean isValid(
Object[] value,
ConstraintValidatorContext context) {
if (value[0] == null || value[1] == null) {
return true;
}
if (!(value[0] instanceof LocalDate)
|| !(value[1] instanceof LocalDate)) {
throw new IllegalArgumentException(
"Illegal method signature, expected two parameters of type LocalDate.");
}
return ((LocalDate) value[0]).isAfter(LocalDate.now())
&& ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}
As we can see, the isValid() method contains the actual validation logic. First, we make sure that we get two parameters of type LocalDate. After that, we check whether both are in the future and end is after begin.
Also, it’s important to notice that the @SupportedValidationTarget(ValidationTarget*.*PARAMETERS) annotation on the ConsistentDateParameterValidator class is required. The reason for this is because @ConsistentDateParameter is set on method-level, but the constraints shall be applied to the method parameters (and not to the return value of the method, as we’ll discuss in the next section).
Note: the Bean Validation specification recommends to consider null-values as valid. If null isn’t a valid value, the @NotNull-annotation should be used instead.
2.4. Return Value Constraints
Sometimes we’ll need to validate an object as it is returned by a method. For this, we can use return value constraints.
The following example uses built-in constraints:
public class ReservationManagement {
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers() {
return null;
}
}
For getAllCustomers(), the following constraints apply:
- First, the returned list must not be null and must have at least one entry
- Furthermore, the list must not contain null entries
2.5. Return Value Custom Constraints
In some cases, we might also need to validate complex objects:
public class ReservationManagement {
@ValidReservation
public Reservation getReservationsById(int id) {
return null;
}
}
In this example, a returned Reservation object must satisfy the constraints defined by @ValidReservation, which we’ll define next.
Again, we first have to define the constraint annotation:
@Constraint(validatedBy = ValidReservationValidator.class)
@Target({ METHOD, CONSTRUCTOR })
@Retention(RUNTIME)
@Documented
public @interface ValidReservation {
String message() default "End date must be after begin date "
+ "and both must be in the future, room number must be bigger than 0";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
After that, we define the validator class:
public class ValidReservationValidator
implements ConstraintValidator<ValidReservation, Reservation> {
@Override
public boolean isValid(
Reservation reservation, ConstraintValidatorContext context) {
if (reservation == null) {
return true;
}
if (!(reservation instanceof Reservation)) {
throw new IllegalArgumentException("Illegal method signature, "
+ "expected parameter of type Reservation.");
}
if (reservation.getBegin() == null
|| reservation.getEnd() == null
|| reservation.getCustomer() == null) {
return false;
}
return (reservation.getBegin().isAfter(LocalDate.now())
&& reservation.getBegin().isBefore(reservation.getEnd())
&& reservation.getRoom() > 0);
}
}
2.6. Return Value in Constructors
As we defined METHOD and CONSTRUCTOR as target within our ValidReservation interface before, we can also annotate the constructor of Reservation to validate constructed instances:
public class Reservation {
@ValidReservation
public Reservation(
LocalDate begin,
LocalDate end,
Customer customer,
int room) {
this.begin = begin;
this.end = end;
this.customer = customer;
this.room = room;
}
// properties, getters, and setters
}
2.7. Cascaded Validation
Finally, the Bean Validation API allows us to not only validate single objects but also object graphs, using the so-called cascaded validation.
Hence, we can use @Valid for a cascaded validation, if we want to validate complex objects. This works for method parameters as well as for return values.
Let’s assume that we have a Customer class with some property constraints:
public class Customer {
@Size(min = 5, max = 200)
private String firstName;
@Size(min = 5, max = 200)
private String lastName;
// constructor, getters and setters
}
A Reservation class might have a Customer property, as well as further properties with constraints:
public class Reservation {
@Valid
private Customer customer;
@Positive
private int room;
// further properties, constructor, getters and setters
}
If we now reference Reservation as a method parameter, we can force the recursive validation of all properties:
public void createNewCustomer(@Valid Reservation reservation) {
// ...
}
As we can see, we use @Valid at two places:
- On the reservation-parameter: it triggers the validation of the Reservation-object, when createNewCustomer() is called
- As we have a nested object graph here, we also have to add a @Valid on the customer-attribute: thereby, it triggers the validation of this nested property
This also works for methods returning an object of type Reservation:
@Valid
public Reservation getReservationById(int id) {
return null;
}
3. Validating Method Constraints
After the declaration of constraints in the previous section, we can now proceed to actually validate these constraints. For that, we have multiple approaches.
3.1. Automatic Validation With Spring
Spring Validation provides an integration with Hibernate Validator.
Note: Spring Validation is based on AOP and uses Spring AOP as the default implementation. Therefore, validation only works for methods, but not for constructors.
If we now want Spring to validate our constraints automatically, we have to do two things:
Firstly, we have to annotate the beans, which shall be validated, with @Validated:
@Validated
public class ReservationManagement {
public void createReservation(@NotNull @Future LocalDate begin,
@Min(1) int duration, @NotNull Customer customer){
// ...
}
@NotNull
@Size(min = 1)
public List<@NotNull Customer> getAllCustomers(){
return null;
}
}
Secondly, we have to provide a MethodValidationPostProcessor bean:
@Configuration
@ComponentScan({ "org.baeldung.javaxval.methodvalidation.model" })
public class MethodValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
The container now will throw a jakarta.validation.ConstraintViolationException, if a constraint is violated.
If we are using Spring Boot, the container will register a MethodValidationPostProcessor bean for us as long as hibernate-validator is in the classpath.
3.2. Automatic Validation With CDI (JSR-365)
As of version 1.1, Bean Validation works with CDI (Contexts and Dependency Injection for Jakarta EE).
If our application runs in a Jakarta EE container, the container will validate method constraints automatically at the time of invocation.
3.3. Programmatic Validation
For manual method validation in a standalone Java application, we can use the jakarta.validation.executable.ExecutableValidator interface.
We can retrieve an instance using the following code:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
ExecutableValidator offers four methods:
- validateParameters() and validateReturnValue() for method validation
- validateConstructorParameters() and validateConstructorReturnValue() for constructor validation
Validating the parameters of our first method createReservation() would look like this:
ReservationManagement object = new ReservationManagement();
Method method = ReservationManagement.class
.getMethod("createReservation", LocalDate.class, int.class, Customer.class);
Object[] parameterValues = { LocalDate.now(), 0, null };
Set<ConstraintViolation<ReservationManagement>> violations
= executableValidator.validateParameters(object, method, parameterValues);
Note: The official documentation discourages to call this interface directly from the application code, but to use it via a method interception technology, like AOP or proxies.
In case you are interested how to use the ExecutableValidator interface, you can have a look at the official documentation.
4. Conclusion
In this tutorial, we had a quick look at how to use method constraints with Hibernate Validator, also we discussed some new features of JSR-380.
First, we discussed how to declare different types of constraints:
- Single parameter constraints
- Cross-parameter
- Return value constraints
We also had a look at how to validate the constraints manually and automatically using Spring Validator.
As always, the full source code of the examples is available over on GitHub.