4

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.constraints annotations. 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.Validator interface. 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.

dmitryb
  • 281
  • 2
  • 12
  • can you try this inside the supports method: `PasswordPair.class.isAssignableFrom(clazz);` – zakaria amine Apr 24 '18 at 10:50
  • 1
    @zakariaamine no, that doesn't help, `PasswordPairValidator` is still applied to `UserInfo` object during `createUser` call. – dmitryb Apr 25 '18 at 07:09

2 Answers2

1

You have to provided an argument to your @InitBinder annotation. Please refer this question

Above question also answers your other question on registering multiple validators.

Prateek Pande
  • 495
  • 3
  • 12
0

I believe the reason this happens is because the @InitBinder method will be called every time a request is being processed and thus for all the methods you have that correspond to HTTP verbs.

The only way I know that you can limit the times the method annotated with @InitBinder gets called is by using the value argument that the annotation takes. I admit that I am also a bit confused on what that value is or how it is interpreted.

Spring boot uses supports to check if a validator can be used every time initBinder() gets called but will throw an exception when it doesn't fit. This happens when initBinder() get called when a Request is processed. So even if you have multiple validators from which one is valid for the request body it will fail

If someone could help with how we can correctly apply validators in Spring boot I would also appreciate it. In C# I know that you can register beans as in middleware and based on the class you register the validator with, the correct validator gets called. (I am not well versed in C# but this is what I remember). Isn't something like this also possible in Java?

Andreas Andreou
  • 818
  • 10
  • 31