Custom validation annotation for multiple types

Some validation constraint can be applied to several different types. For instance the @Size annotation works with collections, arrays, and strings. This post is a guide on how to create a validation constraint which can be applied to different data types just like the aforementioned @Size annotation.

Advertisement

1. Validation subject

For the purpose of the demo we are going to develop a constraint for data types which verifies possible days of a week. The constraint will be applicable to LocalDate from Java 8 and the plain vanilla Date type. Below there is a class required for verification.

public class DateRequest {

    @DaysOfWeek(days = {SATURDAY, SUNDAY})
    private Date weekendDay;
    @DaysOfWeek(days = {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY})
    private LocalDate workingDay;

    // constructor and getters

}

2. Constrain annotation

The marker annotation is almost identical to the example from the previous post. The only dissimilarity is the list of classes which actually do validation.

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {DaysOfWeekLocalDateValidator.class, DaysOfWeekDateValidator.class})
public @interface DaysOfWeek {
    String message() default "{com.dolszewski.blog.DaysOfWeek.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    DayOfWeek[] days() default {DayOfWeek.MONDAY};
}

3. Validator base class

Although each supported type requires a separate validator class, it would be unwise to repeat the similar code in every validator implantation. In most cases common logic can be extracted to a base abstract class.

public abstract class DaysOfWeekValidator<T> implements ConstraintValidator<DaysOfWeek, T> {

    private Set<DayOfWeek> validDays;

    @Override
    public void initialize(DaysOfWeek constraint) {
        validDays = Arrays.stream(constraint.days()).collect(toSet());
    }

    @Override
    public boolean isValid(T value, ConstraintValidatorContext context) {
        if (value == null) return false;
        LocalDate localDate = toLocalDate(value);
        return validDays.contains(localDate.getDayOfWeek());
    }

    protected abstract LocalDate toLocalDate(T value);

}

A base abstract validator class is a good place to cover topics which are the same in all possible implementations:

  • the declaration of a served constraint annotation using generic attribute of the ConstraintValidator interface (DaysOfWeek in the line 1),
  • storage of annotation input parameters for validation purpose (lines 3 – 8),
  • common validation logic (lines 10 – 17).

Our sample utilizes the template method pattern toLocalDate() to make sure each sub class will provide a conversion method from a validated type to LocalDate which output is verified in the isValid() method. Such a pattern is not a panacea for all possible constraints as sometimes conversion to a common type might be too expensive or even impossible. It is advised to consider each case individually.

4. Validator implementations

The validator implementation for DateType is very simple as conversion of validated type to the base type is not required.

public class DaysOfWeekLocalDateValidator extends DaysOfWeekValidator<LocalDate> {

    @Override
    protected LocalDate toLocalDate(LocalDate value) {
        return value;
    }

}

But for the Date class validator, we need to provide simple conversion logic.

public class DaysOfWeekDateValidator extends DaysOfWeekValidator<Date> {

    @Override
    protected LocalDate toLocalDate(Date value) {
        return value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }

}

We may cover more data types in the similar fashion. Just implement a new validator class which extends the base DaysOfWeekValidator class and put it on the list in @Constraint declared in our custom @DaysOfWeek annotation.

5. Unit testing

The constraint and validators are ready so now we can verify if our solution works. Similarly to the previous post, we first acquire an instance of the JSR 303 validator. Next, a sample invalid object is created. Finally, the object is validated and the expected results are confirmed.

public class DateRequestTest {

    private Validator validator;

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

    @Test
    public void shouldMarkAllDatesInvalid() throws Exception {
        // given
        Date notWeekendDate = new GregorianCalendar(2016, Calendar.SEPTEMBER, 20).getTime();
        LocalDate notWorkingDate = LocalDate.of(2016, 9, 18);
        DateRequest request = new DateRequest(notWeekendDate, notWorkingDate);
        // when
        Set<ConstraintViolation<DateRequest>> violations = validator.validate(request);
        // then
        assertEquals(2, violations.size());
    }

}

6. Summary

As you could see creating a constraint for multiple types is pretty straightforward. All you have to provide are one constraint annotation and a validator of each type that you want to verify. Fortunately, common code base of validators may be pulled up to an abstract base class. If you would like to see full code samples from this tutorial, you should check the GitHub repository. Also, please do not hesitate to leave a comment or a question below the post.

Facebooktwittergoogle_plusredditlinkedinmail
Advertisement