Custom bean validation constraints

Bean Validation API defines several built-in constraint annotations which are very useful in many situations. However, there are still some cases where these standard constraints are not enough and you have to create your own custom constraint. With Bean Validation this task is pretty easy and straightforward.

In the article Validating HTML forms in Spring using Bean Validation we have built a simple Spring MVC application using built-in constraints only. This time we will introduce a new text form field for entering favourite day of a week (in place of the age field) and add a custom constraint to this field. The new field will be checked if it a part of workweek or weekend depending on the attributes set in the constraint. Additionally, the constraint will allow specifying whether the comparison is case-sensitive or not.

Defining custom annotation

The first step is creating a custom annotation @DayOfWeek which represents a custom constraints:

package com.example.beanvalidationcustomconstraint;

import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

@Documented
@Constraint(validatedBy = DayOfWeekValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface DayOfWeek {
    String message() default "{com.example.beanvalidationcustomconstraint.DayOfWeek.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    DayOfWeekType[] value() default { };
    boolean ignoreCase() default false;
}

The annotation contains three mandatory attributes: message, groups and payload. The first attribute specifies a message to show or a reference to it if the validation fails. In this case the message attribute references the actual message stored in ValidationMessages.properties file or one of it internationalized versions. Attribute groups allows the definition of validation groups but we won’t use any in this example. The last one payload specifies extra data to be used by the clients of this constraint – we also do not use any in this example.

The are also two other attributes which are more interesting from our point of view and are used to provide additional settings for the custom constraint. The value is a default attribute (used when no other attribute name is specified when using the annotation) and in our case it holds an array of allowed day types:

package com.example.beanvalidationcustomconstraint;

public enum DayOfWeekType {
    WORKWEEK,
    WEEKEND
}

The ignoreCase attribute specifies whether the constraint should use case-sensitive or case-insensitive string comparisons. If these attributes are not specified, they default to an empty array and false, respectively.

We also annotate the newly created annotation with @Documented to enable showing it in JavaDoc for elements annotated with it, @Constraint to indicate that this is a Bean Validation constraint annotation and to specify the custom validator associated with it, @Target to inform that the annotation can be attached to methods, fields and other annotations and @Retention to specify that we want the annotation to be available at runtime via reflection.

Defining the validator

The created annotation does not contain logic which performs actual validation but instead it refers to the class DayOfWeekValidator using @Constraint annotation. The actual validator looks like this:

package com.example.beanvalidationcustomconstraint;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class DayOfWeekValidator implements ConstraintValidator<DayOfWeek, String> {
    private DayOfWeekType[] allowedTypes;
    private boolean ignoreCase;
    
    @Override
    public void initialize(DayOfWeek constraint) {
        allowedTypes = constraint.value();
        ignoreCase = constraint.ignoreCase();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null)
            return true;
        
        for (DayOfWeekType type : allowedTypes) {
            switch (type) {
                case WORKWEEK:
                    if (isWorkWeek(value))
                        return true;
                    break;
                case WEEKEND:
                    if (isWeekEnd(value))
                        return true;
            }
        }
        return false;
    }

    private boolean isWorkWeek(String value) {
        return equalsDay(value, "Monday") || equalsDay(value, "Tuesday")
                || equalsDay(value, "Wednesday") || equalsDay(value, "Thursday")
                || equalsDay(value, "Friday");
    }

    private boolean isWeekEnd(String value) {
        return equalsDay(value, "Saturday") || equalsDay(value, "Sunday");
    }

    private boolean equalsDay(String value1, String value2) {
        return ignoreCase ? value1.equalsIgnoreCase(value2) : value1.equals(value2);
    }

}

The custom validator implements generic ConstraintValidator interface with two type parameters: the type of the custom constraint annotation and the type of the element which can be validated using this validator. Then we implement initialize() method which fetches the attributes/settings of the custom constraint and isValid() method which performs the actual validation and returns true if the validation finished successfully or false otherwise.

Using the constraint

Once we have the annotation and the validator ready, we can use it in the same way as any other built-in constraint:

package com.example.beanvalidationcustomconstraint;

import java.io.Serializable;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

public class Person implements Serializable {
    private static final long serialVersionUID = 3297423984732894L;
    
    @Size(min = 1, max = 20, message = "{firstNameInvalid}")
    private String firstName;
    @Size(min = 1, max = 40, message = "{lastNameInvalid}")
    private String lastName;

    @NotNull
    @DayOfWeek(value = DayOfWeekType.WEEKEND, ignoreCase = true)
    private String favouriteDayOfWeek;

    // constructor, setters and getters
}

In this case we allow only Saturday and Sunday (ignoring the letter case) as the value of favouriteDayOfWeek field. Because we use Spring MVC, the validation will take place when the user tries to submit the form.

Defining custom constraints using composition

Sometimes we don’t even need to define a validator for the custom constraint. It is possible if we can represent our custom constraint as a conjunction of already existing constraints. In this example we specify a constraint which is met only if @NotNull, @Min and @Max constraints are met:

package com.example.beanvalidationcustomconstraint;

import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

@NotNull
@Min(0)
@Max(10)
@Documented
@Constraint(validatedBy = {})
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Range {
    String message() default "Range is not valid";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

}

This is especially useful if the same combination of constraints are applied to many different fields or methods.

Conclusion

Bean Validation is very extensible and allows us to define virtually any custom constraint and use it in the same way as the built-in ones.

The sample code for this example was tested with JBoss and is available at GitHub.

Advertisement

About Robert Piasecki

Husband and father, Java software developer, Linux and open-source fan.
This entry was posted in Java, Java EE, Spring and tagged , , , . Bookmark the permalink.

4 Responses to Custom bean validation constraints

  1. Pingback: Validating HTML forms in Spring using Bean Validation | softwarecave

  2. Pingback: Custom annotations in Java | softwarecave

  3. You could certainly see your expertise in the
    article you write. The sector hopes for even more passionate writers such
    as you who aren’t afraid to mention how they believe. All
    the time go after your heart.

  4. lolo says:

    Shouldn’t it be :
    if (value == null)
    return false; //instead of return true? If null, I am assuming it shouldn’t be valid?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.