Cross field validation in Spring and JEE

All built-in JSR 303 constraint annotations are intended to verify particular fields of our data classes. Yet, it is not unusual that several fields are connected to each other and should be checked as a unity.

For instance, a field can be required only if another field is set. @NotNull won’t work in such case as there is no way to introduce the condition logic. In this post you will learn how to write a validator applicable to multiple class fields.

Advertisement

1. Simple solution

As usual in programming the problem can be solved in several ways. We start with a basic implementation and then consider a more general approach.

1.1. Simple data class

Our study case will be based upon a typical registration form with two password fields that should be equal.

@EqualPasswords
public class UserSignUpForm {

    private String login;
    private String password;
    private String confirmedPassword;

    // Constructor and getters

}

You may have an idea that a custom constrain annotation should be put on the main password field and have a property which somehow refer to the password confirmation field, but that is not a case.

Multiple field validation restrictions are described using a class level constraint annotation.

1. 2. Class level annotation

The annotation doesn’t differ significantly from the basic example in the first post of the validation series. The only thing worth mentioning is the attribute of the @Target annotation which limits application of our custom annotation only to class level.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EqualPasswordsValidator.class})
public @interface EqualPasswords {

    String message() default "{com.dolszewski.blog.EqualPasswords.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

1. 3. Class validator

In order to write our verification logic, we must create a class which implements the ConstrainValidator interface. The first generic parameter is our annotation and the second is the annotated data class.

public class EqualPasswordsValidator implements ConstraintValidator<EqualPasswords, UserSignUpForm> {

    @Override
    public void initialize(EqualPasswords constraint) {
    }

    @Override
    public boolean isValid(UserSignUpForm form, ConstraintValidatorContext context) {
        return form.getPassword().equals(form.getConfirmedPassword());
    }

}

As you see, in the isValid() method we have access to an instance of the validated class, hence we can compare two password fields.

1. 4. Unit testing

Our implementation can be confirmed using a simple JUnit test. We create an invalid object and after the validation step check if the error is present.

public class UserSignUpFormTest {

    private Validator validator;

    @Before
    public void setUp() throws Exception {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.getValidator();
    }

    @Test
    public void shouldMarkPasswordsAsInvalid() throws Exception {
        //given
        UserSignUpForm form = new UserSignUpForm("daniel", "pass", "differentPass");
        //when
        Set<ConstraintViolation<UserSignUpForm>> violations = validator.validate(form);
        //then
        assertEquals(1, violations.size());
    }

}

The assertion step is very naïve as we don’t really know which constraint has been breached, but for demonstration purpose it should be enough.

2. Towards more generic approach

The solution presented above has one very obvious disadvantage: the constraint annotation can be applied only to one class, hence it is completely unreusable. It would be better if we could use it on any class and allow to parametrize which fields should be compared.

2.1. Modified constraint annotation

In search for the more flexible solution the annotation must be changed to accept attributes with the field names. Also the naming is adjusted to reflect its purpose more clearly.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EqualFieldsValidator.class})
public @interface EqualFields {

    String message() default "{com.dolszewski.blog.EqualFields.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String baseField();

    String matchField();

}

2.2. Applying modified annotation

The data class is almost the same. We only add values for required attributes of the updated annotation.

@EqualFields(baseField = "password", matchField = "confirmedPassword")
public class UserSignUpForm {

    private String login;
    private String password;
    private String confirmedPassword;

    // Constructor and getters

}

2.3. Generic validator

The biggest changes are introduced to the validator class. In the first line you see that now instead of a particular class we set the validated type to Object. The annotation attributes are stored in the initialize() method (you can read more about the parametrized validators in the previous post). Validation logic became more complex as now it uses reflection to get values of validated fields.

public class EqualFieldsValidator implements ConstraintValidator<EqualFields, Object> {

    private String baseField;
    private String matchField;

    @Override
    public void initialize(EqualFields constraint) {
        baseField = constraint.baseField();
        matchField = constraint.matchField();
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {
        try {
            Object baseFieldValue = getFieldValue(object, baseField);
            Object matchFieldValue = getFieldValue(object, matchField);
            return baseFieldValue != null && baseFieldValue.equals(matchFieldValue);
        } catch (Exception e) {
            // log error
            return false;
        }
    }

    private Object getFieldValue(Object object, String fieldName) throws Exception {
        Class<?> clazz = object.getClass();
        Field passwordField = clazz.getDeclaredField(fieldName);
        passwordField.setAccessible(true);
        return passwordField.get(object);
    }

}

2.4. Generic approach tradeoff and alternative

The solution isn’t perfect as it relies on the reflection mechanism. If the name of a field in the data class is changed, the corresponding string attribute in the declared constraint annotation have to be altered as well. The compiler won’t warn about the discrepancy, hence more attention from a programmer is required.

But not all is lost.

The problem can be avoided by writing a proper unit test which confirms validation is executed on each data class that use the annotation.

An alternative safer solution would be to base the validator class upon some interface which would return values of comparable fields. With such approach the constraint annotation could be used without attributes like in the basic example, but the data class would have to implement the interface. It would result in aliases for getters in the data class which may pollute your public API of the data class or even possibly break mechanisms of some libraries utilized in a project which rely on accessors.

3. Wrapping things up

All steps required to create a validator for multiple class fields are similar to those required for a single field constraint. The main difference is that instead of verifying an isolated property value we have access to a whole instance of a validated class.

Depending on a particular case, the design for reusability of a cross field validator might be a considerable challenge. This post described two possible options with their pros and cons. You can visit the GitHub repository to see the whole code with the presented examples.

If you like to discuss the mentioned topic or have a question related to the proposed solutions, I strongly encourage you to post a comment or contact with me.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement

4 Replies to “Cross field validation in Spring and JEE”

      1. Hmm.. didn’t see it in debugger. But I found more elegant way:

        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addPropertyNode(baseField).addConstraintViolation();

        After that it will be a FieldError which could be passed regular way.

Leave a Reply