Multiple constraint annotations confused on Java Bean Validation

4.1k views Asked by At

I am confused about the case that have multiple constraint annotations on a field, below:

    public class Student
{
    @NotNull
    @Size(min = 2, max = 14, message = "The name '${validatedValue}' must be between {min} and {max} characters long")
    private String name;

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }
}

Test case:

public class StudentTest
{
    private static Validator validator;

    @BeforeClass
    public static void setUp()
    {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        validator = factory.getValidator();

        System.out.println(Locale.getDefault());
    }

    @Test
    public void nameTest()
    {
        Student student = new Student();
        student.setName(null);

        Set<ConstraintViolation<Student>> constraintViolations = validator.validateProperty(student, "name");

        System.out.println(constraintViolations.size());
        System.out.println(constraintViolations.iterator().next().getMessage());

    }

}

The result is:

1 
Can't be null

That is, when the @NotNull constraint is violated, it will not continue. Yes, this is the right situation. When one check is failed, we don't want it check the next constraint. But the situation is different when I used custom constraint.

I defined two custom constraints ACheck and BCheck.

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { ACheckValidator.class })
public @interface ACheck
{
    String message() default "A check error";

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

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

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { BCheckValidator.class })
public @interface BCheck
{
    String message() default "B check error";

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

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

public class ACheckValidator implements ConstraintValidator<ACheck, String>
{

    public void initialize(ACheck constraintAnnotation)
    {
    }

    public boolean isValid(String value, ConstraintValidatorContext context)
    {
        return false;
    }

}

public class BCheckValidator implements ConstraintValidator<BCheck, String>
{

    public void initialize(BCheck constraintAnnotation)
    {
    }

    public boolean isValid(String value, ConstraintValidatorContext context)
    {
        return false;
    }

}

There is not specific info about custom constraint, and I change the Student.java and use custom constraint like that:

@ACheck
@BCheck
private String name;

Test again, and the result is:

2
B check error

That is, when the @ACheck constraint is violatedm, it also wil check @BCheck, Why this happens, anything else I had ignored?

2

There are 2 answers

1
JB Nizet On BEST ANSWER

when the @NotNull constraint is violated, it will not continue

That is incorrect. It will continue checking all the other constraints. It's just that the Size validator considers a null value as an acceptable value. The reason is that typically, you want

  • a non-null, minimum size value: then you apply both constraints
  • or a nullable value, which must have a minimum size if the value is present: then you only apply Size.
0
Makoto On

You're misunderstanding those validators - they have no guarantee of order in which they are evaluated.

By default, constraints are evaluated in no particular order, regardless of which groups they belong to.

So that means that either your ACheck or your BCheck could have failed, or both; it's not determined which failure will occur first.

If you want to be able to define an ordering with two distinct annotations, then you would have to use a @GroupSequence to specify that.

Alternatively, if you want to fail fast, then configure the validator to do so.

Validator validator = Validation.byProvider( HibernateValidator.class )
        .configure()
        .failFast( true )
        .buildValidatorFactory()
        .getValidator();

I would personally discourage that approach as it implies that a user that fails validations must make repeated requests to the resource every time one thing is wrong, as opposed to getting everything that is wrong up front.