Spring Boot binding and validation error handling in REST controller
Solution 1
This is the code what i have used in one of my project for validating REST api in spring boot,this is not same as you demanded,but is identical.. check if this helps
@RequestMapping(value = "/person/{id}",method = RequestMethod.PUT)
@ResponseBody
public Object updatePerson(@PathVariable Long id,@Valid Person p,BindingResult bindingResult){
if (bindingResult.hasErrors()) {
List<FieldError> errors = bindingResult.getFieldErrors();
List<String> message = new ArrayList<>();
error.setCode(-2);
for (FieldError e : errors){
message.add("@" + e.getField().toUpperCase() + ":" + e.getDefaultMessage());
}
error.setMessage("Update Failed");
error.setCause(message.toString());
return error;
}
else
{
Person person = personRepository.findOne(id);
person = p;
personRepository.save(person);
success.setMessage("Updated Successfully");
success.setCode(2);
return success;
}
Success.java
public class Success {
int code;
String message;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
Error.java
public class Error {
int code;
String message;
String cause;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getCause() {
return cause;
}
public void setCause(String cause) {
this.cause = cause;
}
}
You can also have a look here : Spring REST Validation
Solution 2
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
}
}
Solution 3
You can't get BindException with @RequestBody. Not in the controller with an Errors
method parameter as documented here:
Errors, BindingResult For access to errors from validation and data binding for a command object (that is, a @ModelAttribute argument) or errors from the validation of a @RequestBody or @RequestPart arguments. You must declare an Errors, or BindingResult argument immediately after the validated method argument.
It states that for @ModelAttribute
you get binding AND validation errors and for your @RequestBody
you get validation errors only.
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
And it was discussed here:
For me it still does not make sense from a user point of view. It is often very important to get the BindExceptions to show the user a proper error message. The argument is, you should do client side validation anyway. But this is not true if a developer is using the API directly.
And imagine your client side validation is based on an API request. You want to check if a given date is valid based on a saved calendar. You send the date and time to the backend and it just fails.
You can modify the exception you get with an ExceptionHAndler reacting on HttpMessageNotReadableException, but with this exception I do not have proper access to which field was throwing the error as with a BindException. I need to parse the exception message to get access to it.
So I do not see any solution, which is kind of bad because with @ModelAttribute
it is so easy to get binding AND validation errors.
Solution 4
I've given up on this; it is just not possible to get the binding errors using @RequestBody
without a lot of custom code. This is different from controllers binding to plain JavaBeans arguments because @RequestBody
uses Jackson to bind instead of the Spring databinder.
See https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22
Solution 5
One of the main blocker for solving this problem is the default eagerly-failing nature of the jackson data binder; one would have to somehow convince it to continue parsing instead of just stumble at first error. One would also have to collect these parsing errors in order to ultimately convert them to BindingResult
entries. Basically one would have to catch, suppress and collect parsing exceptions, convert them to BindingResult
entries then add these entries to the right @Controller
method BindingResult
argument.
The catch & suppress part could be done by:
- custom jackson deserializers which would simply delegate to the default related ones but would also catch, suppress and collect their parsing exceptions
- using
AOP
(aspectj version) one could simply intercept the default deserializers parsing exceptions, suppress and collect them - using other means, e.g. appropriate
BeanDeserializerModifier
, one could also catch, suppress and collect the parsing exceptions; this might be the easiest approach but requires some knowledge about this jackson specific customization support
The collecting part could use a ThreadLocal
variable to store all necessary exceptions related details. The conversion to BindingResult
entries and the addition to the right BindingResult
argument could be pretty easily accomplished by an AOP
interceptor on @Controller
methods (any type of AOP
, Spring variant including).
What's the gain
By this approach one gets the data binding errors (in addition to the validation ones) into the BindingResult
argument the same way as would expect for getting them when using an e.g. @ModelAttribute
. It will also work with multiple levels of embedded objects - the solution presented in the question won't play nice with that.
Solution Details (custom jackson deserializers approach)
I created a small project proving the solution (run the test class) while here I'll just highlight the main parts:
/**
* The logic for copying the gathered binding errors
* into the @Controller method BindingResult argument.
*
* This is the most "complicated" part of the project.
*/
@Aspect
@Component
public class BindingErrorsHandler {
@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void logBefore(JoinPoint joinPoint) {
// copy the binding errors gathered by the custom
// jackson deserializers or by other means
Arrays.stream(joinPoint.getArgs())
.filter(o -> o instanceof BindingResult)
.map(o -> (BindingResult) o)
.forEach(errors -> {
JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
});
});
// errors copied, clean the ThreadLocal
JsonParsingFeedBack.ERRORS.remove();
}
}
/**
* The deserialization logic is in fact the one provided by jackson,
* I only added the logic for gathering the binding errors.
*/
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
/**
* Jackson based deserialization logic.
*/
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return wrapperInstance.deserialize(p, ctxt);
} catch (InvalidFormatException ex) {
gatherBindingErrors(p, ctxt);
}
return null;
}
// ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}
/**
* A simple classic @Controller used for testing the solution.
*/
@RestController
@RequestMapping("/errormixtest")
@Slf4j
public class MixBindingAndValidationErrorsController {
@PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) {
// at the end I show some BindingResult logging for a @RequestBody e.g.:
// {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
// ... your whatever logic here ...
With these you'll get in BindingResult
something like this:
Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]
where the 1th line is determined by a validation error (setting 1
as the value for a @Min(5) private Integer nr12;
) while the 2nd is determined by a binding one (setting "x"
as value for a @JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;
). 3rd line tests binding errors with embedded objects: level1
contains a level2
which contains a level3
object property.
Note how other approaches could simply replace the usage of custom jackson deserializers while keeping the rest of the solution (AOP
, JsonParsingFeedBack
).
Jaap van Hengstum
Updated on July 09, 2022Comments
-
Jaap van Hengstum almost 2 years
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:
- Is custom code the way to go here or is there a more standard way of doing this in Spring Boot?
- How does the
@Validated
annotation work? How can I make my own custom annotation that works like@Validated
to encapsulate my custom binding code?
-
Jaap van Hengstum over 8 yearsDisadvantage here is that you don't get the BindingResult when a binding error occurs. I.e. you can do
ex.getBindingResult()
onMethodArgumentNotValidException
exception, but not on aHttpMessageNotReadableException
exception. -
Ali Dehghani over 8 yearsThe latter seems reasonable, because when binding is failing, we could not have a binding result. There is no binding.
-
Jaap van Hengstum over 8 yearsIn my view binding errors like putting a String in a int field or a wrong Enum value should be treated as validation errors. Using
DataBinder
standalone also binding field errors are in theBindingResult
so the service can return a more detailed error response. -
Kevin M almost 7 yearsI agree. I'm having an issue trying to properly show a GOOD error message when someone provides an invalid value for an enum. Jackson wraps it and I end up with a very generic HttpMessageNotReadableException. The message in there includes java package and class information in it. Not acceptable. I want to know the field that failed and why, but I can't find any way to do that. I've tried turning off the WRAP_EXCEPTIONS setting but that doesn't seem to have any effect
-
Janning over 5 yearsBut you can't get the BindException if teh user enters "a" for a field of integer.
-
Adrian almost 5 yearsThis won't work with
@RequestBody
e.g when receiving someJSON
- otherwise a very common case forREST
based web services (also the point of the question).