Grouping Javax Validation Constraints

1. Introduction

In our Java Bean Validation Basics tutorial, we saw the usage of various built-in javax.validation constraints. In this tutorial, we’ll see how to group javax.validation constraints.

2. Use Case

There are many scenarios where we need to apply constraints on a certain set of fields of the bean, and then later we want to apply constraints on another set of fields of the same bean.

For example, let us imagine that we have a two-step signup form. In the first step, we ask the user to provide basic information like the first name, last name, email id, phone number, and captcha. When the user submits this data, we want to validate this information only.

In the next step, we ask the user to provide some other information like an address, and we want to validate this information as well — note that captcha is present in both steps.

3. Grouping Validation Constraints

All javax validation constraints have an attribute named groups. When we add a constraint to an element, we can declare the name of the group to which the constraint belongs. This is done by specifying the class name of the group interface in the groups attributes of the constraint.

The best way to understand something is to get our hands dirty. Let’s see in action how we combine javax constraints into groups.

3.1. Declaring Constraint Groups

The first step is to create some interfaces. These interfaces will be the constraint group names. In our use-case, we’re dividing validation constraints into two groups.

Let’s see the first constraint group, BasicInfo:

public interface BasicInfo {
}

The next constraint group is AdvanceInfo:

public interface AdvanceInfo {
}

3.2. Using Constraint Groups

Now that we’ve declared our constraint groups, it’s time to use them in our RegistrationForm Java bean:

public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;
    @NotBlank(groups = BasicInfo.class)
    private String lastName;
    @Email(groups = BasicInfo.class)
    private String email;
    @NotBlank(groups = BasicInfo.class)
    private String phone;

    @NotBlank(groups = {BasicInfo.class, AdvanceInfo.class})
    private String captcha;

    @NotBlank(groups = AdvanceInfo.class)
    private String street;

    @NotBlank(groups = AdvanceInfo.class)
    private String houseNumber;

    @NotBlank(groups = AdvanceInfo.class)
    private String zipCode;

    @NotBlank(groups = AdvanceInfo.class)
    private String city;

    @NotBlank(groups = AdvanceInfo.class)
    private String contry;
}

With the constraint groups attribute, we have divided the fields of our bean into two groups according to our use case. By default, all constraints are included in the Default constraint group.

3.3. Testing Constraints Having One Group

Now that we’ve declared constraint groups and used them in our bean class, it’s time to see these constraint groups in action.

First, we’ll see when basic information is not complete, using our BasicInfo constraint group for validation. We should get a constraint violation for any field left blank where we used BasicInfo.class in the groups attribute of the field’s @NotBlank constraint:

public class RegistrationFormUnitTest {
    private static Validator validator;

    @BeforeClass
    public static void setupValidatorInstance() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForBasicInfo() {
        RegistrationForm form = buildRegistrationFormWithBasicInfo();
        form.setFirstName("");

        Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, BasicInfo.class);

        assertThat(violations.size()).isEqualTo(1);
        violations.forEach(action -> {
            assertThat(action.getMessage()).isEqualTo("must not be blank");
            assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
        });
    }

    private RegistrationForm buildRegistrationFormWithBasicInfo() {
        RegistrationForm form = new RegistrationForm();
        form.setFirstName("devender");
        form.setLastName("kumar");
        form.setEmail("[email protected]");
        form.setPhone("12345");
        form.setCaptcha("Y2HAhU5T");
        return form;
    }

    //... additional tests
}

In the next scenario, we’ll check when the advanced information is incomplete, using our AdvanceInfo constraint group for validation:

@Test
public void whenAdvanceInfoIsNotComplete_thenShouldGiveConstraintViolationsOnlyForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setZipCode("");

    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, AdvanceInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("zipCode");
    });
}

private RegistrationForm buildRegistrationFormWithAdvanceInfo() {
    RegistrationForm form = new RegistrationForm();
    return populateAdvanceInfo(form);
}

private RegistrationForm populateAdvanceInfo(RegistrationForm form) {
    form.setCity("Berlin");
    form.setContry("DE");
    form.setStreet("alexa str.");
    form.setZipCode("19923");
    form.setHouseNumber("2a");
    form.setCaptcha("Y2HAhU5T");
    return form;
}

3.4. Testing Constraints Having Multiple Groups

We can specify multiple groups for a constraint. In our use case, we’re using captcha in both basic and advanced info. Let’s first test the captcha with BasicInfo:

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForBasicInfo() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setCaptcha("");

    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, BasicInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

Now let’s test the captcha with AdvanceInfo:

@Test
public void whenCaptchaIsBlank_thenShouldGiveConstraintViolationsForAdvanceInfo() {
    RegistrationForm form = buildRegistrationFormWithAdvanceInfo();
    form.setCaptcha("");

    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, AdvanceInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("captcha");
    });
}

4. Specifying Constraint Group Validation Order with GroupSequence

By default, the constraint groups are not evaluated in any particular order. But we may have use cases where some groups should be validated before others. For achieving this, we can specify the order of group validation using GroupSequence. 

There are two ways of using the GroupSequence annotation:

  • on the entity being validated

  • on an Interface

4.1. Using GroupSequence on the Entity Being Validated

This is a simple way of ordering the constraints. Let’s annotate the entity with GroupSequence and specify the order of constraints:

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public class RegistrationForm {
    @NotBlank(groups = BasicInfo.class)
    private String firstName;
    @NotBlank(groups = AdvanceInfo.class)
    private String street;
}

4.2. Using GroupSequence on an Interface

We can also specify the order of constraint validation using an interface. The advantage of this approach is that the same sequence can be used for other entities. Let’s see how we can use GroupSequence with the interfaces we defined above:

@GroupSequence({BasicInfo.class, AdvanceInfo.class})
public interface CompleteInfo {
}

4.3. Testing GroupSequence

Now let’s test GroupSequence. First, we will test that if BasicInfo is incomplete, then the AdvanceInfo group constraint will not be evaluated:

@Test
public void whenBasicInfoIsNotComplete_thenShouldGiveConstraintViolationsForBasicInfoOnly() {
    RegistrationForm form = buildRegistrationFormWithBasicInfo();
    form.setFirstName("");

    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, CompleteInfo.class);

    assertThat(violations.size()).isEqualTo(1);
    violations.forEach(action -> {
        assertThat(action.getMessage()).isEqualTo("must not be blank");
        assertThat(action.getPropertyPath().toString()).isEqualTo("firstName");
    });
}

Next, test that when BasicInfo is complete, then the AdvanceInfo constraint should be evaluated:

@Test
public void whenBasicAndAdvanceInfoIsComplete_thenShouldNotGiveConstraintViolationsWithCompleteInfoValidationGroup() {
    RegistrationForm form = buildRegistrationFormWithBasicAndAdvanceInfo();

    Set<ConstraintViolation<RegistrationForm>> violations = validator.validate(form, CompleteInfo.class);

    assertThat(violations.size()).isEqualTo(0);
}

5. Conclusion

In this quick tutorial, we saw how to group javax.validation constraints.

As usual, all code snippets are available over on GitHub.