Validation groups in Spring MVC

All constraints in Bean Validation may be added to one or more groups via groups attribute. This allows you to restrict the set of constraints applied during validation. It can be handy in cases where some groups should be validated before others like e.g. in wizards. As of Spring MVC 3.1, automatic validation utilizing validation groups is possible with org.springframework.validation.annotation.Validated annotation. In this article I will use simple Spring MVC application to demonstrate how easily you can use validation groups to validate Spring’s MVC model attributes.

Form

Let’s start with the form class that will be validated in steps. Firstly, we define interfaces that represents constraint groups:

public class Account implements PasswordAware {

    interface ValidationStepOne {
        // validation group marker interface
    }

    interface ValidationStepTwo {
        // validation group marker interface
    }
}

Validation contraints

Next we assign constraint to groups. Remember, if you don’t provide groups the default one will be used. Please also note @SamePasswords, @StrongPassword - custom constraints, that must define groups attribute:

@SamePasswords(groups = {Account.ValidationStepTwo.class})
public class Account implements PasswordAware {

    @NotBlank(groups = {ValidationStepOne.class})
    private String username;

    @Email(groups = {ValidationStepOne.class})
    @NotBlank(groups = {ValidationStepOne.class})
    private String email;

    @NotBlank(groups = {ValidationStepTwo.class})
    @StrongPassword(groups = {ValidationStepTwo.class})
    private String password;

    @NotBlank(groups = {ValidationStepTwo.class})
    private String confirmedPassword;

    // getters and setters
}

Wizard

Having the Account, we can create a 3-step wizard @Controller that will let users create an account. In first step we will let Spring validate constraint in ValidationStepOne group:

@Controller
@RequestMapping("validationgroups")
@SessionAttributes("account")
public class AccountController {

    @RequestMapping(value = "stepOne")
    public String stepOne(Model model) {
        model.addAttribute("account", new Account());
        return VIEW_STEP_ONE;
    }

    @RequestMapping(value = "stepOne", method = RequestMethod.POST)
    public String stepOne(@Validated(Account.ValidationStepOne.class) Account account, Errors errors) {
        if (errors.hasErrors()) {
            return VIEW_STEP_ONE;
        }
        return "redirect:stepTwo";
    }
}

To trigger validation with groups I used @Validated annotation. This annotation takes var-arg argument with groups’ types. The code @Validated(ValidationStepOne.class) triggers validation of constraint in ValidationStepOne group.

In the next step we will let Spring validate constraint in ValidationStepTwo group:

@Controller
@RequestMapping("validationgroups")
@SessionAttributes("account")
public class AccountController {

    @RequestMapping(value = "stepTwo")
    public String stepTwo() {
        return VIEW_STEP_TWO;
    }

    @RequestMapping(value = "stepTwo", method = RequestMethod.POST)
    public String stepTwo(@Validated(Account.ValidationStepTwo.class) Account account, Errors errors) {
        if (errors.hasErrors()) {
            return VIEW_STEP_TWO;
        }
        return "redirect:summary";
    }
}

In the summary step we will confirm entered data and we will let Spring validate constraint of both groups:

@Controller
@RequestMapping("validationgroups")
@SessionAttributes("account")
public class AccountController {

    @RequestMapping(value = "summary")
    public String summary() {
        return VIEW_SUMMARY;
    }

    @RequestMapping(value = "confirm")
    public String confirm(@Validated({Account.ValidationStepOne.class, Account.ValidationStepTwo.class}) Account account, Errors errors, SessionStatus status) {
        status.setComplete();
        if (errors.hasErrors()) {
            // did not pass full validation
        }
        return "redirect:start";
    }
}

Prior to Spring 3.1 you could trigger the validation manually. I described this in one of my previous posts: http://blog.codeleak.pl/2011/03/how-to-jsr303-validation-groups-in.html

Note: If you want to use validation groups without Spring, you need to pass groups to javax.validation.Validator#validate():

Validation groups without Spring

Validator validator = Validation
        .buildDefaultValidatorFactory().getValidator();
Account account = new Account();

// validate with first group
Set<ConstraintViolation<Account>> constraintViolations =
        validator.validate(account, Account.ValidationStepOne.class);
assertThat(constraintViolations).hasSize(2);

// validate with both groups
Set<ConstraintViolation<Account>> constraintViolations =
        validator.validate(account, Account.ValidationStepOne.class, Account.ValidationStepTwo.class);
assertThat(constraintViolations).hasSize(4);

This is also the easiest way to test validations:

public class AccountValidationTest {

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Test
    public void shouldHaveFourConstraintViolationsWhileValidatingBothGroups() {
        Account account = new Account();
        Set<ConstraintViolation<Account>> constraintViolations = validator.validate(
                account, Account.ValidationStepOne.class, Account.ValidationStepTwo.class
        );
        assertThat(constraintViolations).hasSize(4);
    }

    @Test
    public void shouldHaveTwoConstraintViolationsWhileStepOne() {
        Account account = new Account();
        Set<ConstraintViolation<Account>> constraintViolations = validator.validate(
                account, Account.ValidationStepOne.class
        );
        assertThat(constraintViolations).hasSize(2);

    }
}

Testing validation with Spring Test

Testing validation with Spring Test offers more sophisticated way of testing if validation/binding failed. For the examples, have a look at my other blog post: Spring MVC Integration Testing: Assert the given model attribute(s) have global errors

The source code for this article can be found here: https://github.com/kolorobot/spring-mvc-beanvalidation11-demo

Popular posts from this blog

Parameterized tests in JavaScript with Jest

macOS: Insert current date shortcut with `Shortcuts.app`