When I have the following model with JSR-303 (validation framework) annotations:
public enum Gender {
MALE, FEMALE
}
public class Profile {
private Gender gender;
@NotNull
private String name;
...
}
and the following JSON data:
{ "gender":"INVALID_INPUT" }
In my REST controller, I want to handle both the binding errors (invalid enum value for gender
property) and validation errors (name
property cannot be null).
The following controller method does NOT work:
@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, BindingResult result) {
...
}
This gives com.fasterxml.jackson.databind.exc.InvalidFormatException
serialization error before binding or validation takes place.
After some fiddling, I came up with this custom code which does what I want:
@RequestMapping(method = RequestMethod.POST)
public Profile insert(@RequestBody Map values) throws BindException {
Profile profile = new Profile();
DataBinder binder = new DataBinder(profile);
binder.bind(new MutablePropertyValues(values));
// validator is instance of LocalValidatorFactoryBean class
binder.setValidator(validator);
binder.validate();
// throws BindException if there are binding/validation
// errors, exception is handled using @ControllerAdvice.
binder.close();
// No binding/validation errors, profile is populated
// with request values.
...
}
Basically what this code does, is serialize to a generic map instead of model and then use custom code to bind to model and check for errors.
I have the following questions:
@Validated
annotation work? How can I make my own custom annotation that works like @Validated
to encapsulate my custom binding code?Usually when Spring MVC fails to read the http messages (e.g. request body), it will throw an instance of HttpMessageNotReadableException
exception. So, if spring could not bind to your model, it should throw that exception. Also, if you do NOT define a BindingResult
after each to-be-validated model in your method parameters, in case of a validation error, spring will throw a MethodArgumentNotValidException
exception. With all this, you can create ControllerAdvice
that catches these two exceptions and handles them in your desirable way.
@ControllerAdvice(annotations = {RestController.class})
public class UncaughtExceptionsControllerAdvice {
@ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
public ResponseEntity handleBindingErrors(Exception ex) {
// do whatever you want with the exceptions
}
}