validationhibernate-validatorbean-validation

Cross field validation with Hibernate Validator (JSR 303)


Is there an implementation of (or third-party implementation for) cross field validation in Hibernate Validator 4.x? If not, what is the cleanest way to implement a cross field validator?

As an example, how can you use the API to validate two bean properties are equal (such as validating a password field matches the password verify field).

In annotations, I'd expect something like:

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}

Solution

  • Each field constraint should be handled by a distinct validator annotation, or in other words it's not suggested practice to have one field's validation annotation checking against other fields; cross-field validation should be done at the class level. Additionally, the JSR-303 Section 2.2 preferred way to express multiple validations of the same type is via a list of annotations. This allows the error message to be specified per match.

    For example, validating a common form:

    @FieldMatch.List({
            @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
            @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
    })
    public class UserRegistrationForm  {
        @NotNull
        @Size(min=8, max=25)
        private String password;
    
        @NotNull
        @Size(min=8, max=25)
        private String confirmPassword;
    
        @NotNull
        @Email
        private String email;
    
        @NotNull
        @Email
        private String confirmEmail;
    }
    

    The Annotation:

    package constraints;
    
    import constraints.impl.FieldMatchValidator;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.Documented;
    import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
    import static java.lang.annotation.ElementType.TYPE;
    import java.lang.annotation.Retention;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    import java.lang.annotation.Target;
    
    /**
     * Validation annotation to validate that 2 fields have the same value.
     * An array of fields and their matching confirmation fields can be supplied.
     *
     * Example, compare 1 pair of fields:
     * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
     * 
     * Example, compare more than 1 pair of fields:
     * @FieldMatch.List({
     *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
     *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Constraint(validatedBy = FieldMatchValidator.class)
    @Documented
    public @interface FieldMatch
    {
        String message() default "{constraints.fieldmatch}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        /**
         * @return The first field
         */
        String first();
    
        /**
         * @return The second field
         */
        String second();
    
        /**
         * Defines several <code>@FieldMatch</code> annotations on the same element
         *
         * @see FieldMatch
         */
        @Target({TYPE, ANNOTATION_TYPE})
        @Retention(RUNTIME)
        @Documented
                @interface List
        {
            FieldMatch[] value();
        }
    }
    

    The Validator:

    package constraints.impl;
    
    import constraints.FieldMatch;
    import org.apache.commons.beanutils.BeanUtils;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    
    public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
    {
        private String firstFieldName;
        private String secondFieldName;
    
        @Override
        public void initialize(final FieldMatch constraintAnnotation)
        {
            firstFieldName = constraintAnnotation.first();
            secondFieldName = constraintAnnotation.second();
        }
    
        @Override
        public boolean isValid(final Object value, final ConstraintValidatorContext context)
        {
            try
            {
                final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
                final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
    
                return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
            }
            catch (final Exception ignore)
            {
                // ignore
            }
            return true;
        }
    }