Spring Handling boot errors and validation in a REST controller

When I have the following model with JSR-303 annotations (validation):

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 binding errors (invalid enum value for the 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 the error <serialization com.fasterxml.jackson.databind.exc.InvalidFormatException before the binding or validation occurs.

After some attempts, I came up with this code that 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 serializes into a common map instead of a model, and then uses its own code to bind to the model and check for errors.

I have the following questions:

  • Is custom code the way here, or is there a more standard way to do this in Spring Boot?
  • How does the @Validated annotation @Validated ? How can I make my own annotation that works like @Validated to encapsulate my own anchor code?
+8
source share
6 answers

This is the code that I used in one of my projects to test the REST API during spring boot, it is not what you required, but it 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 look here: Spring REST Validation

+5
source

Usually, when Spring MVC cannot read HTTP messages (for example, the request body), it will throw an instance of an HttpMessageNotReadableException . Thus, if Spring cannot communicate with your model, it should throw this exception. In addition, if you do NOT define a BindingResult after each model to be verified in the method parameters, in case of a validation error, Spring throws a MethodArgumentNotValidException exception. With all this, you can create a ControllerAdvice that catches these two exceptions and processes them as you wish.

 @ControllerAdvice(annotations = {RestController.class}) public class UncaughtExceptionsControllerAdvice { @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class}) public ResponseEntity handleBindingErrors(Exception ex) { // do whatever you want with the exceptions } } 
+2
source

I have given up on this; it is simply not possible to get binding errors using @RequestBody without a lot of custom code. This is different from binding controllers to simple JavaBeans arguments, because @RequestBody uses Jackson to bind instead of a Spring data binding.

See https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22

+2
source

You cannot get a BindException with @RequestBody. Not in the controller with the Errors method parameter as described here:

Errors, BindingResult To access errors when checking and binding data for the command object (i.e., the @ModelAttribute argument) or errors when checking the @RequestBody or @RequestPart arguments. You must declare the Errors or BindingResult argument immediately after the argument of the proven method.

It says that for @ModelAttribute you get binding and validation errors, and for your @RequestBody you only get validation errors .

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

And it was discussed here:

https://github.com/spring-projects/spring-framework/issues/11406?jql=text%2520~%2520%2522RequestBody%2520binding%2522

For me, this still doesn't make sense from a user perspective. It is often very important that BindExceptions show the user the correct error message. The argument is that you should still perform client-side validation. But this is not so if the developer uses the API directly.

And imagine that 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 doesn't work.

You can change the exception you get with an ExceptionHAndler that responds to an HttpMessageNotReadableException, but with this exception I do not have proper access to the field in which the error occurred, as in the case of BindException. I need to parse the exception message in order to access it.

So I do not see any solution, which is partly bad, because with @ModelAttribute so easy to get binding and validation errors.

+2
source

According to this post https://blog.codecentric.de/en/2017/11/dynamic-validation-spring-boot-validation/ - you can add an additional "Errors" parameter to your controller method, for example.

 @RequestMapping(method = RequestMethod.POST) public Profile insert(@Validated @RequestBody Profile profile, Errors errors) { ... } 

then get validation errors, if any, at that.

0
source

One of the main obstacles to solving this problem is the default failure of Jackson's data binding mechanism; one would have to somehow convince him to continue the analysis, and not just stumble at the first error. You also need to collect these parsing errors in order to ultimately convert them to BindingResult records. Essentially, you would have to catch, suppress, and collect exceptions, convert them to BindingResult records BindingResult then add these records to the correct @Controller method.

Part capture and suppression can be performed:

  • Jackson custom deserializers that will simply delegate the default ones, but will also catch, suppress, and collect their exceptions when parsing
  • using AOP (aspectj version), you can simply intercept the default deserializers, analyze exceptions, suppress and collect them
  • using other means, for example, the corresponding BeanDeserializerModifier , you can also catch, suppress and collect exceptions during parsing; this may be the easiest approach, but it requires some knowledge of Jackson’s tuning support

Part of the collection can use the ThreadLocal variable to store all the necessary details related to exceptions. Converting to BindingResult entries and adding BindingResult to the right argument can be quite easily done by the AOP interceptor in @Controller methods (any type of AOP , including the Spring variant).

What a win

With this approach, data binding errors (in addition to validation errors) BindingResult argument BindingResult in the same way as when receiving, for example, @ModelAttribute . It will also work with several levels of embedded objects - the solution presented in this question is not suitable for this.

Solution Details (Jackson Custom Deserializers Approach)

I created a small project proving the solution (run the test class), and here I 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 eg: // {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}} // ... your whatever logic here ... 

With this, you will get something like this in BindingResult :

 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 1st line is determined by the verification error (setting 1 as the value for @Min(5) private Integer nr12; ), while the 2nd @Min(5) private Integer nr12; defined by binding (setting "x" as the value for @JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11; ). The third line tests binding errors with built-in objects: level1 contains level2 which contains level3 object.

Note that other approaches may simply replace the use of custom Jackson deserializers while preserving the rest of the solution ( AOP , JsonParsingFeedBack ).

0
source

Source: https://habr.com/ru/post/1240370/


All Articles