Spring MVC: How to Perform Validation

Spring MVC: How to perform validation?

With Spring MVC, there are 3 different ways to perform validation : using annotation, manually, or a mix of both. There is not a unique "cleanest and best way" to validate, but there is probably one that fits your project/problem/context better.

Let's have a User :

public class User {

private String name;

...

}

Method 1 : If you have Spring 3.x+ and simple validation to do, use javax.validation.constraints annotations (also known as JSR-303 annotations).

public class User {

@NotNull
private String name;

...

}

You will need a JSR-303 provider in your libraries, like Hibernate Validator who is the reference implementation (this library has nothing to do with databases and relational mapping, it just does validation :-).

Then in your controller you would have something like :

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @Valid @ModelAttribute("user") User user, BindingResult result){
if (result.hasErrors()){
// do something
}
else {
// do something else
}
}

Notice the @Valid : if the user happens to have a null name, result.hasErrors() will be true.

Method 2 : If you have complex validation (like big business validation logic, conditional validation across multiple fields, etc.), or for some reason you cannot use method 1, use manual validation. It is a good practice to separate the controller’s code from the validation logic. Don't create your validation class(es) from scratch, Spring provides a handy org.springframework.validation.Validator interface (since Spring 2).

So let's say you have

public class User {

private String name;

private Integer birthYear;
private User responsibleUser;
...

}

and you want to do some "complex" validation like : if the user's age is under 18, responsibleUser must not be null and responsibleUser's age must be over 21.

You will do something like this

public class UserValidator implements Validator {

@Override
public boolean supports(Class clazz) {
return User.class.equals(clazz);
}

@Override
public void validate(Object target, Errors errors) {
User user = (User) target;

if(user.getName() == null) {
errors.rejectValue("name", "your_error_code");
}

// do "complex" validation here

}

}

Then in your controller you would have :

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @ModelAttribute("user") User user, BindingResult result){
UserValidator userValidator = new UserValidator();
userValidator.validate(user, result);

if (result.hasErrors()){
// do something
}
else {
// do something else
}
}

If there are validation errors, result.hasErrors() will be true.

Note : You can also set the validator in a @InitBinder method of the controller, with "binder.setValidator(...)" (in which case a mix use of method 1 and 2 would not be possible, because you replace the default validator). Or you could instantiate it in the default constructor of the controller. Or have a @Component/@Service UserValidator that you inject (@Autowired) in your controller : very useful, because most validators are singletons + unit test mocking becomes easier + your validator could call other Spring components.

Method 3 :
Why not using a combination of both methods? Validate the simple stuff, like the "name" attribute, with annotations (it is quick to do, concise and more readable). Keep the heavy validations for validators (when it would take hours to code custom complex validation annotations, or just when it is not possible to use annotations). I did this on a former project, it worked like a charm, quick & easy.

Warning : you must not mistake validation handling for exception handling. Read this post to know when to use them.

References :

  • A very interesting blog post about bean validation (Original link is dead)
  • Another good blog post about validation (Original link is dead)
  • Latest Spring documentation about validation

How to perform custom validations in Spring MVC?

Validation can be tricky and difficult, some things to take into account when doing validation...

Validation Considerations

  • The model in MVC (Model, View, Controller) does not, and generally should not, be the same as your Domain Model. See @wim-deblauwe's comment and the Q&A section below.

    • Often times what is displayed in the user interface is different than what is available inside the Domain Model.
    • Placing @Valid annotations into your Domain Model means that in every form that Domain Model is used, the same @Valid rules will apply. This is not always true. Side Note: This might not apply to super simple CRUD (Create, Read, Update, Delete) applications, but in general, most applications are more sophisticated that just pure CRUD.
  • There are SERIOUS security issues with using a real Domain Model object as the form backing object due to the way that Spring does auto setting of values during form submittal. For example, if we are using a User object that has a password field on it as our form backing object, the form could be manipulated by browser developer tools to send a new value for the password field and now that new value will get persisted.

  • All data entered via an html form is really String data that will need to get transposed to its real data type (Integer, Double, Enumeration, etc...) later.

  • There are different types of validation that, in my opinion, need to happen in different temporal order.

    • Required checks happen before type checking (Integer, Double, Enumeration, etc...), valid value ranges, and then finally persistence checks (uniqueness, previous persisted values, etc...)
    • If there are any errors in a temporal level, then don't check anything later.
      • This stops the end user from getting errors like, phone number is required, phone number isn't a number, phone number isn't formatted correctly, etc... in the same error message.
  • There shouldn't be any temporal coupling between validators. Meaning that if a field is optional, then 'data type' validator shouldn't fail validation if a value isn't present. See the validators below.

Example

Domain Object / Business Object:

@Entity
public class Person {

private String identifier;
private String name;
private int year;

public String getIdentifier() {
return identifier;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}
}

To populate an html form via a Spring MVC Controller we would create a specific object that represents that form. This also includes all the validation rules as well:

@GroupSequence({Required.class, Type.class, Data.class, Persistence.class, CreateOrUpdatePersonForm.class})
public class CreateOrUpdatePersonForm {

@NotBlank(groups = Required.class, message = "Name is required.")
private String name;

@NotBlank(groups = Required.class, message = "Year is required.")
@ValidInteger(groups = Type.class, message = "Year must be a number.")
@ValidDate(groups = Data.class, message = "Year must be formatted yyyy.")
private String year;

public CreateOrUpdatePersonForm(Person person) {
this.name = person.getName();
this.year = Integer.valueOf(person.getYear);
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getYearStr() {
this.year;
}

public void setYearStr(String year) {
this.year = year;
}

public int getYear() {
return Integer.valueOf(this.year);
}
}

Then in your controller to use the new CreateOrUpdatePersonForm object:

@Controller
public class PersonController {
...
@ModelAttribute("command")
public CreateOrUpdatePersonForm setupCommand(@RequestParam("identifier") Person person) {

return new CreateOrUpdatePersonForm(person);
}

//@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.GET)
public ModelAndView getForm(@RequestParam("person") Person person) {

return new ModelAndView("/form/person");
}

//@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.POST)
public ModelAndView postForm(@RequestParam("person") Person person, @ModelAttribute("command") @Valid CreateOrUpdatePersonForm form,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {

ModelAndView modelAndView;

if (bindingResult.hasErrors()) {

modelAndView = new ModelAndView("/form/person");

} else {

this.personService.updatePerson(person.getIdentifier(), form);

redirectAttributes.addFlashAttribute("successMessage", "Person updated.");

modelAndView = new ModelAndView("redirect:/person/" + person.getIdentifier() + ".html");
}

return modelAndView;
}
}

The @ValidInteger and @ValidDate are validators that we wrote ourselves.

@ValidInteger:

public class ValidIntegerValidator implements ConstraintValidator<ValidInteger, String> {

@Override
public void initialize(ValidInteger annotation) {

}

@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {

boolean valid = true;

if (StringUtils.hasText(value)) {

try {
Integer.parseInteger
(value);

} catch (NumberFormatException e) {

valid = false;
}
}

return valid;
}
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidIntegerValidator.class)
@Documented
public @interface ValidInteger {

String message() default "{package.valid.integer}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

@ValidDate

public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {

private String format;

@Override
public void initialize(ValidDate annotation) {
this.format = annotation.format();
}

@Override
public boolean isValid(String inputDate, ConstraintValidatorContext constraintValidatorContext) {

boolean valid = true;

if (StringUtils.hasText(inputDate)) {

SimpleDateFormat dateFormat = new SimpleDateFormat(format);

dateFormat.setLenient(false);

try {

dateFormat.parse(inputDate);

} catch (ParseException e) {

valid = false;
}
}

return valid;
}
}

@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidDateValidator.class)
@Documented
public @interface ValidDate {

String message() default "{package.dateformat}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

String format();
}

Then in your view jsp or template you'll need to display the errors if there are any:

<html>
...
<body>
<common:form-errors modelAttribute="command"/>
...
</body>
</html>

There is a lot more to deal with validation, like comparing two fields together, or accessing your persistence layer to verify that a person name is unique, but that takes a lot more explanation.

Q & A

Q: Can you provide links that explain the thought behind not using the Domain Model as the MVC Model?

A: Sure, Entities VS Domain Models VS View Models and Entity vs Model vs View Model

TL;DR: Using different objects for the Domain Model and the MVC Model is because it reduces coupling between application layers and it protects our UI and Domain Model from changes in either layer.

Other Considerations

Validation of data needs to occur at all entry points to an application: UI, API, and any external systems or files that are read in.

An API is just a UI for computers and needs to follow the same rules as the human UI.

Accepting data from the internet is fraught with peril. It is better to be more restrictive than less restrictive. This also includes making sure that there aren't any strange characters coughMicrosoft's 1252 character encodingcough, Sql Injection, JavaScript Injection, making sure that your database is setup for unicode and understanding that a column that is setup for 512 characters, depending on the language can only actually handle 256 characters due to codepoints.

How to do validation in Spring MVC when there's a DTO?

Found the solution for this.
One simple change.
Just need to add @Valid in the DTO class.
This is the only line that needs to be updated:
private List<@Valid Person> personList;

Spring MVC and @Validate: Perform validate only on specific condition or if user changes the property

  1. Removed the @Valid from the address field
  2. Did the validation manually: inside the create method of the controller: validateAddressIfNeeded(person, bindingResult)

    private void validateAddressIfNeeded(Person person, BindingResult bindingResult) {
    if (person.hasAddress()) {
    bindingResult.pushNestedPath("address");
    validator.validate(person.getAddress(), bindingResult);
    bindingResult.popNestedPath();
    }
    }

Validation in Spring MVC

Not 100% sure I'm following your question correctly, but with Spring MVC, you pass the object into the method and annotate it (at least with Spring 3), like so:

@RequestMethod(value = "/accounts/new", method = RequestMethod.POST)
public String postAccount(@ModelAttribute @Valid Account account, BindingResult result) {
if (result.hasErrors()) {
return "accounts/accountForm";
}

accountDao.save(account);
}

The relevant annotation here is @Valid, which is part of JSR-303. Include the BindingResult param as well so you have a way to check for errors, as illustrated above.

How to do validation processing in Spring MVC

Instead of passing the data as request params, you need to pass as the request body.

Can you update the controller method as below and try

@PostMapping("/registration")
ResponseEntity<String> registration(@Valid @RequestBody User user) {
userService.registration(user.getName(), user.getLastName(), user.getLogin(), user.getPassword());
return ResponseEntity.ok("Valid!");
}
@ControllerAdvice
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatus status, WebRequest request) {

Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}

Spring MVC Validation shows multiple error messages

You can use getFieldErrorCount method on the Error error object to check whether there is already an error on the contactNumber field. Like this:

// check integer
if (e.getFieldErrorCount("contactNumber") <= 0 && !p.getContactNumber().matches("[0-9]+")) {
e.rejectValue("contactNumber", "Format.contactNumber");
}

This is the easiest fix with the smallest change set. You should definitely look at @NotNull and @Pattern annotation validations.



Related Topics



Leave a reply



Submit