I'm trying to make sense of how validation works in Spring. So far I've learned that there are two ways to perform validation of user input data:
- JSR-303 validation based on
javax.validation.constraintsannotations. This validation is best suitable for simple object fields validation. But you can also implement your custom type level annotations to perform more complicated validation based on checking values of multiple fields. - Spring Validation based on
org.springframework.validation.Validatorinterface. Seems to be better suited for more complicated validation.
If I want to use both these approaches, what is the right way to register my custom validator in controller?
This is what I'm trying to do.
My custom validator.
public class PasswordPairValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return PasswordPair.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
PasswordPair password = (PasswordPair) target;
if (!password.getPassword().equals(password.getRepeatPassword())) {
errors.reject("passwordField", "passwords don't match");
}
}
}
My controller.
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new PasswordPairValidator());
}
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<UserInfo> createUser(
@RequestBody @Valid UserInfo userInfo) {
userInfo.setId(123);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(userInfo.getId()).toUri();
return ResponseEntity.created(location).body(userInfo);
}
@RequestMapping(value = "/change_password", method = RequestMethod.POST)
public ResponseEntity<UserInfo> changePassword(
@RequestBody @Valid PasswordPair password) {
UserInfo user = new UserInfo("test@gmail.com", "testuser");
user.setId(123);
return ResponseEntity.ok().body(user);
}
}
When I call createUser endpoint the code fails with the following error:
ERROR c.e.testapp.controller.GlobalExceptionHandler - Internal Server Error
java.lang.IllegalStateException: Invalid target for Validator [com.example.testapp.validation.PasswordPairValidator@49acd001]: com.example.testapp.domain.UserInfo@cae4750
The problem apparently is that Spring tries to apply PasswordPairValidator to UserInfo object, which was not my intention.
Why Spring doesn't use validator's supports() method to check to which objects validator can be applied?
In a different stackoverflow question I found out that I need to specify value for @InitBinder annotation to make it work and the value should be "passwordPair". But what is this value as it's not the class name ("PasswordPair") or method parameters value ("password")?
The second question is if I want to add several validators do I need to define multiple @InitBinder("value") methods or is there a less cumbersome way to do it?
And the final question, maybe it's better to use annotation based validation for everything, to validate separate fields and implement type level custom annotations with ConstraintValidator to perform more complicated validation? It's a bit confusing what are the pros and cons of these approaches.