Validating a Spring MVC @Controller with a custom validation class - the easy way

For some reason this is terribly documented on the web .. so, I hope this helps someone.

Assuming we have the following model:

public class PersonForm {

    @NotNull
    @Max(64)
    private String name;
    
    @Min(0)
    private int age;

}

To perform more advanced validation, we could write a custom validator class:


public class PersonFormValidator implements Validator {

    /*
     * This Validator validates *just SomeModel instances
     */
     public boolean supports(Class clazz) {
   	return PersonForm.class.equals(clazz);
     }

     public void validate(Object obj, Errors e) {
                
    	PersonForm personForm = (PersonForm) obj;
        // for the purpose of this demo
        // we're going to reject this value all the time
        e.rejectValue("age", "age.rejected"); 
        
    }

}

Notes:

  • Notice that our validator implements Validator
    • So it also has to implement the methods: supports and validate
  • We use e.rejectValue("<field>", "<message>"); to reject this field

Finally, our controller might look something like this:


@Controller
public class PersonController{
	
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
           binder.setValidator(new PersonFormValidator());
    }
    	
    @RequestMapping(value = "/person", method = RequestMethod.POST)
    public String submitPerson(
        @Valid @ModelAttribute("PersonForm") 
        PersonForm personForm,
        BindingResult binding, 
        HttpSession session,
        RedirectAttributes redirectAttributes) {

	    //validation 
	    if(binding.hasErrors()){
           	return "/pages/person";
            }
	    return "/pages/personSuccess";
        }
}

Notes:

  • @IinitBinder sets the validator for this @Controller to be our PersonFormValidator.
    • Remember: supports(Class clazz) in PersonFormValidator tells the controller which models are validated with this validator
  • The line: @Valid @ModelAttribute("PersonForm") PersonForm personForm tells the controller to validate the PersonForm object. Results from the validation are stored in BindingResult binding
  • The @Valid annotation will validate our model PersonForm using the annotations within the class (e.g.: @NotBlank, @min and @max as well as running the validation defined in PersonFormValidator.validate()
  • Finally, we check if there are errors and deal with them accordingly with binding.hasErrors()

Making sure it all works

import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.server.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.view;

public class PersonControllerTest {

    MockMvc mockMvc;

    @InjectMocks
    PersonController controller;
	
    @Before
    public void setUp() throws Exception {
		
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/view/");
        viewResolver.setSuffix(".jsp");
       
        this.mockMvc = MockMvcBuilders.standaloneSetup(controller)
	    		.setViewResolvers(viewResolver)
	                .setMessageConverters(new MappingJackson2HttpMessageConverter()).build();
    }


    @Test
    public void testSubmitPerson_failsValidation() 
      throws Exception {
		
	this.mockMvc.perform( 
	  post("/person")
		.param("name", "Joe")
		.param("age", "100")
	).andExpect(model()
          .attributeHasFieldErrors("personForm", "age"))
             .andExpect( view().name("/pages/person") )
             .andExpect( status().isOk() )
             .andDo(print());
					
    }
}

Notes:

  • Standard setup for mockMvc in setup()
  • .andExpect(model().attributeHasFieldErrors("personForm", "age")) does most of the work here - it ensures that the "age" field on our "personForm" has an error raised against it.
  • I included the imports for the various Builders, Matchers and Handlers .. cause, for some reason, they're often a little tricksy to find in my IDE (might be the same for you ;) ).

.. and that should do the trick :).

In summary

  • Create a model (PersonForm)
  • Create a custom validation class for validation our model (PersonFormValidator)
  • In your @Controller, bind your validator to the controller using the @InitBinder annotation and binder.setValidator
  • in your @Controller method, Use the @Valid annotation to validate the incoming model.
  • You can test the results with the MockMvc and model().attributeHasFieldErrors() to ensure that validation fails in the correct cases.

References: