Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot binding and validation error handling in REST controller

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:

  1. Is custom code the way to go here or is there a more standard way of doing this in Spring Boot?
  2. How does the @Validated annotation work? How can I make my own custom annotation that works like @Validated to encapsulate my custom binding code?
like image 330
Jaap van Hengstum Avatar asked Jan 11 '16 18:01

Jaap van Hengstum


4 Answers

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

like image 61
al_mukthar Avatar answered Nov 20 '22 14:11

al_mukthar


enter code here
 public class User {

@NotNull
@Size(min=3,max=50,message="min 2 and max 20 characters are alllowed !!")
private String name;

@Email
private String email;

@Pattern(regexp="[7-9][0-9]{9}",message="invalid mobile number")
@Size(max=10,message="digits should be 10")
private String phone;

@Override
public String toString() {
    return "User [name=" + name + ", email=" + email + ", phone=" + phone + "]";
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getEmail() {
    return email;
}

public void setEmail(String email) {
    this.email = email;
}

public String getPhone() {
    return phone;
}

public void setPhone(String phone) {
    this.phone = phone;
}


}

   Controller.java

    @Controller
    public class User_Controller {

    @RequestMapping("/")
    public String showForm(User u,Model m)
    {
    m.addAttribute("user",new User());
    m.addAttribute("title","Validation Form");
    return "register";
    }

    @PostMapping("/")
    public String register(@Valid User user,BindingResult bindingResult ,Model m)
    {
    if(bindingResult.hasErrors())
    {
        return "register";
    }
    else {
        m.addAttribute("message", "Registration successfully... ");
    return "register";
    }
    }
    }
 

   register.html
   <div class="container">
   <div class="alert alert-success" role="alert" th:text="${message}">
   </div>
   <h1 class="text-center">Validation Form </h1>
   <form action="/" th:action="@{/}" th:object="${user}" method="post">
   <div class="mb-3">
   <label for="exampleInputEmail1" class="form-label">Name</label>
   <input type="text" class="form-control" id="exampleInputEmail1" aria- 
    describedby="emailHelp" th:field="*{name}">
    <br>
    <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}" class="alert alert- 
    danger"></p>
    </div>
    <div class="mb-3">
    <label for="exampleInputPassword1" class="form-label">Email</label>
     <input type="email" class="form-control" id="exampleInputPassword1" th:field="* 
    {email}">
    <br>
   <p th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert alert- 
   danger"></p>
   </div>

   <div class="mb-3">
   <label for="exampleInputPassword1" class="form-label">Phone</label>
   <input type="text" class="form-control" id="exampleInputPassword1" th:field="* 
   {phone}">
    <p th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="alert alert- 
    danger"></p>
    <br>
    </div>

    <button type="submit" class="btn btn-primary">Submit</button>
     </form>
     </div>
like image 31
Gaurav Rawat Avatar answered Oct 08 '22 03:10

Gaurav Rawat


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
    }
}
like image 8
Ali Dehghani Avatar answered Nov 20 '22 14:11

Ali Dehghani


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:

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

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.

like image 6
Janning Avatar answered Nov 20 '22 13:11

Janning