Close

JavaBean Validation - Cross parameter constraints

[Last Updated: Aug 25, 2021]

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());
    }
}

Output

TradeHistory.<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());
    }
}

Output

showTradeHistory.<cross-parameter> 'start date' must be less than 'end date'. Found: 'start date'=2021-08-10, 'end date'=2021-07-01

Example Project

Dependencies 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 List
    ×

    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

Cross parameter constraints Examples Select All Download
  • cross-parameter-constraint
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • CrossParameterConstructorExample.java

    See Also