Close

Java Bean Validation - Creating multiple validators for the same constraint

[Last Updated: Aug 25, 2021]

Following example shows how to create multiple validators (multiple implementations of ConstraintValidators) for the same constraint.

Multiple validators for the same constraint might be needed if we want to target different elements specified by SupportedValidationTarget. If two ConstraintValidators refer to the same target type, an exception will occur.

Definition of SupportedValidationTarget

(Version: java-bean-validation 6.2.0.Final)
package javax.validation.constraintvalidation;
   ........
@Documented
@Target({TYPE})
@Retention(RUNTIME)
public @interface SupportedValidationTarget {
    ValidationTarget[] value();
}

Definition of ValidationTarget

(Version: java-bean-validation 6.2.0.Final)
package javax.validation.constraintvalidation;
   ........
public enum ValidationTarget {
    ANNOTATED_ELEMENT, 1
    PARAMETERS 2
}
1 (Returned) element annotated by the constraint.
2 Array of parameters of the annotated method or constructor (aka cross-parameter).

As seen in above definition, we can create two validators annotated with following annotations:
- @SupportedValidationTarget(ValidationTarget.PARAMETERS)
- @SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)

In following example we are going to apply the same constraint two times (each tied to the two different validators implementations) on a single method. One constraint for validating return value (ValidationTarget.ANNOTATED_ELEMENT) of the method and the second constraint for validating parameter (ValidationTarget.PARAMETERS) of the method.

The attribute validationAppliesTo() requirement

When creating a constraint that is both generic (refers to ValidationTarget.ANNOTATED_ELEMENT) and cross-parameter (refers to ValidationTarget.PARAMETERS), the constraint annotation must include the validationAppliesTo() property:

 ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;

This requirement is to remove the ambiguity when using same constraint for two target types.

As seen above, the type of the validationAppliesTo() parameter is ConstraintTarget. The default value must be ConstraintTarget.IMPLICIT.

Definition of ConstraintTarget

(Version: java-bean-validation 6.2.0.Final)
package javax.validation;
   ........
public enum ConstraintTarget {
    IMPLICIT, 1
    RETURN_VALUE, 2
    PARAMETERS 3
}
1 Discover the type when no ambiguity is present
  • if neither on a method nor a constructor, it implies the annotated element (type, field etc),
  • if on a method or constructor with no parameter, it implies RETURN_VALUE,
  • if on a method with no return value (void), it implies PARAMETERS.
Otherwise, IMPLICIT is not accepted and either RETURN_VALUE or PARAMETERS is required. This is the case for constructors with parameters and methods with parameters and return value.
2 Constraint applies to the return value of a method or a constructor.
3 Constraint applies to the parameters of a method or a constructor

Example

Our custom constraint definition

package com.logicbig.example;

import javax.validation.Constraint;
import javax.validation.ConstraintTarget;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EvenNumbersInputValidator.class, EvenNumbersOutputValidator.class})
@Repeatable(EvenNumbers.List.class)
public @interface EvenNumbers {

    String message() default "not valid even numbers: ${validatedValue}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        EvenNumbers[] value();
    }
}

As seen above we used @Repeatable(EvenNumbers.List.class), this is to use @EventNumbers annotation multiple times on the same method.

ConstraintValidator implementation for cross-parameters

package com.logicbig.example;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.util.Arrays;

@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class EvenNumbersInputValidator implements
        ConstraintValidator<EvenNumbers, Object[]> {

    @Override
    public void initialize(EvenNumbers constraintAnnotation) {
    }

    @Override
    public boolean isValid(Object[] value, ConstraintValidatorContext context) {
        if (value == null || value.length != 2 ||
                !(value[0] instanceof Integer) ||
                !(value[1] instanceof Integer)) {
            return false;
        }
        Integer int1 = (Integer) value[0];
        Integer int2 = (Integer) value[1];

        if (int1 % 2 != 0 || int2 % 2 != 0) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("Either of the input parameters are not even " + Arrays
                    .toString(value))
                   .addConstraintViolation();

            return false;
        }
        if (int1.compareTo(int2) > 0) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(String
                    .format("first input %s is not less than second input %s", int1, int2))
                   .addConstraintViolation();
            return false;
        }
        return true;
    }
}

Dynamically creating error messages via ConstraintValidatorContext#buildConstraintViolationWithTemplate()

In above code we used ConstraintValidatorContext#buildConstraintViolationWithTemplate(). This is to create violation messages within the validator. The violation message is interpolated.

Also to disable the default ConstraintViolation object generation (which is using the message template declared on the constraint) we used ConstraintValidatorContext#disableDefaultConstraintViolation().

ConstraintValidator implementation for return value

package com.logicbig.example;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.util.List;

@SupportedValidationTarget(ValidationTarget.ANNOTATED_ELEMENT)
public class EvenNumbersOutputValidator implements
        ConstraintValidator<EvenNumbers, List<Integer>> {

    @Override
    public void initialize(EvenNumbers constraintAnnotation) { }

    @Override
    public boolean isValid(List<Integer> value, ConstraintValidatorContext context) {
        if (value == null) {
            return false;
        }
        for (Integer integer : value) {
            if (integer % 2 != 0) {
                return false;
            }
        }
        return true;
    }
}

Example bean

package com.logicbig.example;

import javax.validation.ConstraintTarget;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class MyBean {

    @EvenNumbers(validationAppliesTo = ConstraintTarget.RETURN_VALUE)
    @EvenNumbers(validationAppliesTo = ConstraintTarget.PARAMETERS)
    public List<Integer> findEvenNumbersBetween(Integer startEven, Integer endEven) {
        return IntStream.range(startEven, endEven + 1)
                        .filter(i -> i % 2 != 0)//purposely make it wrong
                        .boxed()
                        .collect(Collectors.toList());
    }
}

Performing validation

package com.logicbig.example;

import javax.validation.*;
import javax.validation.executable.ExecutableValidator;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;

public class MultipleValidatorMain {

    private static Validator validator;

    public static void main(String[] args) throws NoSuchMethodException {
        System.out.println("-- validating cross param --");
        validateCrossParameters(7, 2);
        validateCrossParameters(6, 2);
        validateCrossParameters(2, 8);

        System.out.println("-- validating return values --");
        validateReturnValue();
    }

    private static void validateReturnValue() throws NoSuchMethodException {
        MyBean myBean = new MyBean();
        List<Integer> returnedValued = myBean.findEvenNumbersBetween(2, 8);
        Method method = MyBean.class.getDeclaredMethod(
                "findEvenNumbersBetween", Integer.class, Integer.class);

        Validator validator = getValidator();
        ExecutableValidator executableValidator = validator.forExecutables();
        Set<ConstraintViolation<MyBean>> violations =
                executableValidator.validateReturnValue(myBean, method, returnedValued);
        if (violations.size() > 0) {
            violations.stream().forEach(MultipleValidatorMain::printError);
        } else {
            System.out.println("returned value validation passed");
            //proceed using object
        }
    }

    private static void validateCrossParameters(int x, int y) throws NoSuchMethodException {
        MyBean myBean = new MyBean();

        Method method = MyBean.class.getDeclaredMethod("findEvenNumbersBetween",
                Integer.class, Integer.class);

        Validator validator = getValidator();
        ExecutableValidator executableValidator = validator.forExecutables();
        Set<ConstraintViolation<MyBean>> violations =
                executableValidator.validateParameters(myBean, method, new Object[]{x, y});
        if (violations.size() > 0) {
            violations.stream().forEach(MultipleValidatorMain::printError);
        } else {
            System.out.println("cross parameter validation passed");
            //proceed using object
        }
    }

    private static Validator getValidator() {
        if (validator == null) {
            Configuration<?> config = Validation.byDefaultProvider().configure();
            ValidatorFactory factory = config.buildValidatorFactory();
            validator = factory.getValidator();
            factory.close();
        }
        return validator;
    }

    private static void printError(
            ConstraintViolation<MyBean> violation) {
        System.out.println(violation.getPropertyPath() + " " + violation.getMessage());
    }
}

Output

-- validating cross param --
findEvenNumbersBetween.<cross-parameter> Either of the input parameters are not even [7, 2]
findEvenNumbersBetween.<cross-parameter> first input 6 is not less than second input 2
cross parameter validation passed
-- validating return values --
findEvenNumbersBetween.<return value> not valid even numbers: [3, 5, 7]

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

Creating multiple validators Select All Download
  • java-bean-multiple-validators-example
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • EvenNumbers.java

    See Also