A constraint annotation is said to be generic if it is used to validate a bean, a field, a getter, a method/constructor return value or a method/constructor parameter. Our all previous examples showed how to use generic constraints.
A constraint is said to be cross-parameter if it is used to validate parameters of a method or constructor. The parameters are passed to the validator via an array. This is usually useful when we want to validate parameters which are dependent on each other.
Cross parameter constraint examples
Using Cross-parameter constraint on constructor
In following example we are going to validate whether the two dates passed to a constructor is in a valid date range, i.e. whether start date (the first parameter) is less than end date (the second parameter).
We are going to create a custom constraint @DateRangeParams. There's no difference between creating a normal constraint vs cross-parameter constraint. The constraint needs to be place on the bean's constructor whose parameters are to be validated
While implementing the validator for our constraint, we have to put an extra @SupportedValidationTarget( ValidationTarget.PARAMETERS ) annotation on it. This is an indication that the validation is to be performed on the constructor/method parameters, not on the return value. If @SupportedValidationTarget is not present then the validation is targeted on the returned value. There's one more difference here, we have to specify multiple parameters as an array of object for the second generic type, so that we can receive all parameters in an array.
package com.logicbig.example;
import javax.validation.*;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.*;
import java.lang.reflect.Constructor;
import java.time.LocalDate;
import java.util.Set;
public class CrossParameterConstructorExample {
//Using cross param constraint on the example bean constructor
public static class TradeHistory {
private final LocalDate startDate;
private final LocalDate endDate;
@DateRangeParams
public TradeHistory (LocalDate startDate, LocalDate endDate) {
this.startDate = startDate;
this.endDate = endDate;
}
public LocalDate getStartDate () {
return startDate;
}
public LocalDate getEndDate () {
return endDate;
}
}
//the constraint definition
@Target({ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
@Documented
public static @interface DateRangeParams {
String message () default "'start date' must be less than 'end date'. " +
"Found: 'start date'=${validatedValue[0]}, " +
"'end date'=${validatedValue[1]}";
Class<?>[] groups () default {};
Class<? extends Payload>[] payload () default {};
}
//the validator implementation
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public static class DateRangeValidator implements
ConstraintValidator<DateRangeParams, Object[]> {
@Override
public void initialize (DateRangeParams constraintAnnotation) {
}
@Override
public boolean isValid (Object[] value, ConstraintValidatorContext context) {
if (value == null || value.length != 2 ||
!(value[0] instanceof LocalDate) ||
!(value[1] instanceof LocalDate)) {
return false;
}
return ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}
//performing validation
public static void main (String[] args) throws NoSuchMethodException {
LocalDate startDate = LocalDate.of(2021, 8, 10);
LocalDate endDate = LocalDate.of(2021, 7, 1);
TradeHistory tradeHistory = new TradeHistory(startDate, endDate);
Constructor<TradeHistory> constructor = TradeHistory.class.getConstructor(LocalDate
.class, LocalDate.class);
Validator validator = getValidator();
ExecutableValidator executableValidator = validator.forExecutables();
Set<ConstraintViolation<TradeHistory>> constraintViolations =
executableValidator.validateConstructorParameters(constructor,
new Object[]{startDate, endDate});
if (constraintViolations.size() > 0) {
constraintViolations.stream().forEach(
CrossParameterConstructorExample::printError);
} else {
//proceed using order
System.out.println(tradeHistory);
}
}
private static Validator getValidator(){
Configuration<?> config = Validation.byDefaultProvider().configure();
ValidatorFactory factory = config.buildValidatorFactory();
Validator validator = factory.getValidator();
factory.close();
return validator;
}
private static void printError (ConstraintViolation<TradeHistory> violation) {
System.out.println(violation.getPropertyPath() + " " + violation.getMessage());
}
}
OutputTradeHistory.<cross-parameter> 'start date' must be less than 'end date'. Found: 'start date'=2021-08-10, 'end date'=2021-07-01
Using Cross-parameter on method
Similar to cross-parameter on constructors, we can do the same for method parameters. We just need to modify our bean accordingly, include 'ElementType.METHOD' to @Target annotation of our constraint and then to perform validation using method executableValidator.validateParameters(..) .
package com.logicbig.example;
import javax.validation.*;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import javax.validation.executable.ExecutableValidator;
import java.lang.annotation.*;
import java.lang.reflect.Method;
import java.time.LocalDate;
import java.util.Set;
public class CrossParameterMethodExample {
//Using cross param constraint on the example bean method
public static class TradeHistoryExecutor {
@DateRangeParams
public void showTradeHistory (LocalDate startDate, LocalDate endDate) {
System.out.printf("processing trade history from %s to %s %n",
startDate, endDate);
}
}
//The constraint definition
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
@Documented
public static @interface DateRangeParams {
String message () default "'start date' must be less than 'end date'. " +
"Found: 'start date'=${validatedValue[0]}, " +
"'end date'=${validatedValue[1]}";
Class<?>[] groups () default {};
Class<? extends Payload>[] payload () default {};
}
//The validator implementation
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public static class DateRangeValidator implements
ConstraintValidator<DateRangeParams, Object[]> {
@Override
public void initialize (DateRangeParams constraintAnnotation) {
}
@Override
public boolean isValid (Object[] value, ConstraintValidatorContext context) {
if (value == null || value.length != 2 ||
!(value[0] instanceof LocalDate) ||
!(value[1] instanceof LocalDate)) {
return false;
}
return ((LocalDate) value[0]).isBefore((LocalDate) value[1]);
}
}
//performing validation
public static void main (String[] args) throws NoSuchMethodException {
LocalDate startDate = LocalDate.of(2021, 8, 10);
LocalDate endDate = LocalDate.of(2021, 7, 1);
TradeHistoryExecutor tradeHistory = new TradeHistoryExecutor();
Method theMethod = TradeHistoryExecutor.class.getDeclaredMethod
("showTradeHistory", LocalDate.class, LocalDate.class);
Validator validator = getValidator();
ExecutableValidator executableValidator = validator.forExecutables();
Set<ConstraintViolation<TradeHistoryExecutor>> constraintViolations =
executableValidator.validateParameters(
tradeHistory,
theMethod,
new Object[]{startDate, endDate});
if (constraintViolations.size() > 0) {
constraintViolations.stream().forEach(
CrossParameterMethodExample::printError);
} else {
//proceed using order
System.out.println(tradeHistory);
}
}
private static Validator getValidator(){
Configuration<?> config = Validation.byDefaultProvider().configure();
ValidatorFactory factory = config.buildValidatorFactory();
Validator validator = factory.getValidator();
factory.close();
return validator;
}
private static void printError (ConstraintViolation<TradeHistoryExecutor> violation) {
System.out.println(violation.getPropertyPath() + " " + violation.getMessage());
}
}
OutputshowTradeHistory.<cross-parameter> 'start date' must be less than 'end date'. Found: 'start date'=2021-08-10, 'end date'=2021-07-01
Example ProjectDependencies and Technologies Used: - hibernate-validator 6.2.0.Final (Hibernate's Jakarta Bean Validation reference implementation)
Version Compatibility: 5.0.0.Final - 6.2.0.Final Version compatibilities of hibernate-validator with this example: groupId: org.hibernate artifactId: hibernate-validator Reference implementation for Bean Validation 1.1
- 5.0.0.Final
- 5.0.1.Final
- 5.0.2.Final
- 5.0.3.Final
- 5.1.0.Final
- 5.1.1.Final
- 5.1.2.Final
- 5.1.3.Final
- 5.2.0.Final
- 5.2.1.Final
- 5.2.2.Final
- 5.2.3.Final
- 5.2.4.Final
- 5.2.5.Final
- 5.3.0.Final
- 5.3.1.Final
- 5.3.2.Final
- 5.3.3.Final
- 5.3.4.Final
- 5.3.5.Final
- 5.3.6.Final
- 5.4.0.Final
- 5.4.1.Final
- 5.4.2.Final
- 5.4.3.Final
groupId: org.hibernate.validator artifactId: hibernate-validator Reference implementation for Bean Validation 2.0
- 6.0.0.Final
- 6.0.1.Final
- 6.0.2.Final
- 6.0.3.Final
- 6.0.4.Final
- 6.0.5.Final
- 6.0.6.Final
- 6.0.7.Final
- 6.0.8.Final
- 6.0.9.Final
- 6.0.10.Final
- 6.0.11.Final
- 6.0.12.Final
- 6.0.13.Final
- 6.0.14.Final
- 6.0.15.Final
- 6.0.16.Final
- 6.0.17.Final
- 6.0.18.Final
- 6.0.19.Final
- 6.0.20.Final
- 6.0.21.Final
- 6.0.22.Final
- 6.1.0.Final
- 6.1.1.Final
- 6.1.2.Final
- 6.1.3.Final
- 6.1.4.Final
- 6.1.5.Final
- 6.1.6.Final
- 6.1.7.Final
- 6.2.0.Final
Version 7 and later: Jakarta Bean Validation 3.0 jakarta.* packages
Versions in green have been tested.
- javax.el-api 3.0.0 (Expression Language 3.0 API)
- javax.el 2.2.6 (Expression Language 2.2 Implementation)
- JDK 8
- Maven 3.8.1
|