In this spring boot exception handler tutorial, we will learn to validate request body sent to PUT/POST REST APIs. We will also learn to add custom error messages in API responses for validation errors.
Table of Contents 1. Create REST APIs and model classes 2. Request validation and exception handling 3. Demo of request validations 4. Available annotations to use 5. Summary
In this spring boot example, we will see primarily two major validation cases –
- HTTP POST
/employees
and request body does not contain valid values or some fields are missing. It will return HTTP status code 400 with proper message in response body. - HTTP GET
/employees/{id}
and INVALID ID is sent in request. It will return HTTP status code 404 with proper message in response body.
1. Create REST APIs and model classes
Given REST APIs are from employee management module.
@PostMapping (value = "/employees" ) public ResponseEntity<EmployeeVO> addEmployee ( @RequestBody EmployeeVO employee) { EmployeeDB.addEmployee(employee); return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK); } @GetMapping (value = "/employees/{id}" ) public ResponseEntity<EmployeeVO> getEmployeeById ( @PathVariable ( "id" ) int id) { EmployeeVO employee = EmployeeDB.getEmployeeById(id); if (employee == null ) { throw new RecordNotFoundException( "Invalid employee id : " + id); } return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK); } |
@XmlRootElement (name = "employee" ) @XmlAccessorType (XmlAccessType.FIELD) public class EmployeeVO extends ResourceSupport implements Serializable { private Integer employeeId; private String firstName; private String lastName; private String email; public EmployeeVO(Integer id, String firstName, String lastName, String email) { super (); this .employeeId = id; this .firstName = firstName; this .lastName = lastName; this .email = email; } public EmployeeVO() { } //Removed setter/getter for readability } |
2. Spring boot exception handling – REST request validation
2.1. Default spring validation support
To apply default validation, we only need to add relevant annotations in proper places. i.e.
- Annotate model class with required validation specific annotations such as
@NotEmpty
,@Email
etc.@XmlRootElement
(name =
"employee"
)
@XmlAccessorType
(XmlAccessType.FIELD)
public
class
EmployeeVO
extends
ResourceSupport
implements
Serializable
{
private
static
final
long
serialVersionUID = 1L;
public
EmployeeVO(Integer id, String firstName, String lastName, String email) {
super
();
this
.employeeId = id;
this
.firstName = firstName;
this
.lastName = lastName;
this
.email = email;
}
public
EmployeeVO() {
}
private
Integer employeeId;
@NotEmpty
(message =
"first name must not be empty"
)
private
String firstName;
@NotEmpty
(message =
"last name must not be empty"
)
private
String lastName;
@NotEmpty
(message =
"email must not be empty"
)
@Email
(message =
"email should be a valid email"
)
private
String email;
//Removed setter/getter for readability
}
- Enable validation of request body by @Valid annotation
@PostMapping
(value =
"/employees"
)
public
ResponseEntity<EmployeeVO> addEmployee (
@Valid
@RequestBody
EmployeeVO employee)
{
EmployeeDB.addEmployee(employee);
return
new
ResponseEntity<EmployeeVO>(employee, HttpStatus.OK);
}
2.2. Exception model classes
Default spring validation works and provide information overload about error, and that’s why we should customize it according to our application’s need. We shall provide only required error information with very clear wordings. Extra information is also not suggested.
It is always a good advise to create exceptions that are meaningful and describe the problem well enough. One way is to create seperate classes to denote specific business usecase failure and return them when that usecase fail.
e.g. I have created RecordNotFoundException
class for all buch scenarios where a resource is requested by it’s ID, and resource is not found in the system.
package com.howtodoinjava.demo.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus (HttpStatus.NOT_FOUND) public class RecordNotFoundException extends RuntimeException { public RecordNotFoundException(String exception) { super (exception); } } |
Similarly, I have wrote an special class which will be returned for all failure cases. Having consistent error message structure for all APIs, help the API consumers to write more robust code.
import java.util.List; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement (name = "error" ) public class ErrorResponse { public ErrorResponse(String message, List<String> details) { super (); this .message = message; this .details = details; } //General error message about nature of error private String message; //Specific errors in API request processing private List<String> details; //Getter and setters } |
2.3. Custom ExceptionHandler
Now add one class extending ResponseEntityExceptionHandler
and annotate it with @ControllerAdvice
annotation.
ResponseEntityExceptionHandler
is a convenient base class for to provide centralized exception handling across all @RequestMapping
methods through @ExceptionHandler
methods. @ControllerAdvice
is more for enabling auto-scanning and configuration at application startup.
Java program for @ControllerAdvice exception handling example.
package com.howtodoinjava.demo.exception; import java.util.ArrayList; import java.util.List; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.ObjectError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @SuppressWarnings ({ "unchecked" , "rawtypes" }) @ControllerAdvice public class CustomExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler (Exception. class ) public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) { List<String> details = new ArrayList<>(); details.add(ex.getLocalizedMessage()); ErrorResponse error = new ErrorResponse( "Server Error" , details); return new ResponseEntity(error, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler (RecordNotFoundException. class ) public final ResponseEntity<Object> handleUserNotFoundException(RecordNotFoundException ex, WebRequest request) { List<String> details = new ArrayList<>(); details.add(ex.getLocalizedMessage()); ErrorResponse error = new ErrorResponse( "Record Not Found" , details); return new ResponseEntity(error, HttpStatus.NOT_FOUND); } @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { List<String> details = new ArrayList<>(); for (ObjectError error : ex.getBindingResult().getAllErrors()) { details.add(error.getDefaultMessage()); } ErrorResponse error = new ErrorResponse( "Validation Failed" , details); return new ResponseEntity(error, HttpStatus.BAD_REQUEST); } } |
Above class handles multiple exceptions including RecordNotFoundException
; and it also handle request validation errors in @RequestBody
annotated object. Let’s see how it works.
3. Spring boot exception handling – Demo
1) HTTP GET /employees/1 [VALID]
HTTP Status : 200 { "employeeId" : 1 , "firstName" : "John" , "lastName" : "Wick" , "email" : "howtodoinjava@gmail.com" , } |
2) HTTP GET /employees/23 [INVALID]
HTTP Status : 404 { "message" : "Record Not Found" , "details" : [ "Invalid employee id : 23" ] } |
3) HTTP POST /employees [INVALID]
{ "lastName" : "Bill" , "email" : "ibill@gmail.com" } |
HTTP Status : 400 { "message" : "Validation Failed" , "details" : [ "first name must not be empty" ] } |
4) HTTP POST /employees [INVALID]
{ "email" : "ibill@gmail.com" } |
HTTP Status : 400 { "message" : "Validation Failed" , "details" : [ "last name must not be empty" , "first name must not be empty" ] } |
5) HTTP POST /employees [INVALID]
{ "firstName" : "Lokesh" , "email" : "ibill_gmail.com" //invalid email in request } |
HTTP Status : 400 { "message" : "Validation Failed" , "details" : [ "last name must not be empty" , "email should be a valid email" ] } |
4. REST request validation annotations
In above example, we used only few annotations such as @NotEmpty
and @Email
. There are more such annotations to validate request data. Check them out when needed.
ANNOTATION | USAGE |
---|---|
@AssertFalse | The annotated element must be false. |
@AssertTrue | The annotated element must be true. |
@DecimalMax | The annotated element must be a number whose value must be lower or equal to the specified maximum. |
@DecimalMin | The annotated element must be a number whose value must be higher or equal to the specified minimum. |
@Future | The annotated element must be an instant, date or time in the future. |
@Max | The annotated element must be a number whose value must be lower or equal to the specified maximum. |
@Min | The annotated element must be a number whose value must be higher or equal to the specified minimum. |
@Negative | The annotated element must be a strictly negative number. |
@NotBlank | The annotated element must not be null and must contain at least one non-whitespace character. |
@NotEmpty | The annotated element must not be null nor empty. |
@NotNull | The annotated element must not be null . |
@Null | The annotated element must be null . |
@Pattern | The annotated CharSequence must match the specified regular expression. |
@Positive | The annotated element must be a strictly positive number. |
@Size | The annotated element size must be between the specified boundaries (included). |
5. Summary
In this spring REST validation tutorial, we learned to –
- validate ID when fetching resource by ID.
- validate request body fields in POST/PUT APIs.
- send consistent and structured error response in API responses.
Drop me your questions related to spring rest exception handling.
No comments:
Post a Comment