JSON:API Error Object with Spring Boot REST API
Objects in JSON:API format have proven themselves as a standardized data exchange with REST. Error handling can also be managed uniformly with JSON:API error objects. Spring Boot offers a standard JSON format for error attributes. In this article, we look at how to change the Spring Boot Standard JSON format to a JSON:API Error Object format. As a bonus, we also find out how the JSON:API Error Object can be displayed instead of the Whitelabel Error Page.
Spring Boot Error Handling
If a Spring Boot application triggers an error, the standard REST response looks as follows:
HTTP/1.1 500
Content-Type: application/json
{
"error": "Internal Server Error",
"message": "500 INTERNAL_SERVER_ERROR",
"path": "/test/internal-server-error",
"status": 500,
"timestamp": "2020-03-20T10:56:08.788+0000"
}
Sample 1: Standard Spring Boot error response.
Compared to a JSON:API Error Object (Example JSON:API Error Object) the standard format differs in some places. We would like the following format for the error response:
HTTP/1.1 500
Content-Type: application/json
{
"errors": [
{
"status": 500,
"source": {
"pointer": "/test/internal-server-error"
},
"title": "Internal Server Error",
"detail": "500 INTERNAL_SERVER_ERROR"
}
]
}
Sample 2: JSON:API error object response.
There are different options to customize the REST error handling from Spring Boot. For instance on the one hand, at the controller level we can use @ExceptionHandler
and on the other hand at global level we can use @ControllerAdvice
.
In this article, we’ll look at a third option: We change the standard error attributes from Spring Boot, so that we end up with a JSON:API Error Object format.
Sample project with JSON:API Error Object
We start with a simple Spring Boot project. As starter dependency we choose ‘Spring Web’. For easier testing we add the ‘Apache HttpClient’ dependency additionally.
First we create the class TestController.class
and add a new endpoint.
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/internal-server-error")
public void getHttpStatusInternalServerError(final HttpServletResponse response) throws IOException {
response.sendError(HttpStatus.SC_INTERNAL_SERVER_ERROR, "500 INTERNAL_SERVER_ERROR");
}
}
Sample 3: TestController.java
If we call the endpoint http://localhost:8080/test/internal-server-error
(e.g. with curl or HTTPie), we get the standard REST error response from Spring Boot:
HTTP/1.1 500
Content-Type: application/json
{
"error": "Internal Server Error",
"message": "500 INTERNAL_SERVER_ERROR",
"path": "/test/internal-server-error",
"status": 500,
"timestamp": "2020-03-20T11:58:53.835+0000"
}
Sample 4: Standard error response from the test endpoint.
Next step: We create the class JsonApiErrorMessage.class
and annotate it with @Component
. In this class we create the method getErrorAttributes
. With this method we can create our individual error response. First we get the error attributes from the super class with super.getErrorAttributes(webRequest, includeStackTrace)
. We put these attributes in a new map jsonApiErrorAttributes
. This map contains the structure exactly as the JSON:API provides for an error in the error object. After that we create an array which contains our map. This is because the JSON:API provides that several errors can be within one error object.
@Component
public class JsonApiErrorMessage extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(final WebRequest webRequest, final boolean includeStackTrace) {
final Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace);
final Map<String, Object> jsonApiErrorAttributes = new LinkedHashMap<>();
jsonApiErrorAttributes.put("status", errorAttributes.get("status"));
jsonApiErrorAttributes.put("source", Collections.singletonMap("pointer", errorAttributes.get("path")));
jsonApiErrorAttributes.put("title", errorAttributes.get("error"));
jsonApiErrorAttributes.put("detail", errorAttributes.get("message"));
return Map.of("errors", new Map[] {jsonApiErrorAttributes});
}
}
Sample 5: Override the standard error attributes from Spring Boot.
If we call the test endpoint http://localhost:8080/test/internal-server-error
again, we get the error response in the desired JSON:API error object format.
HTTP/1.1 500
Content-Type: application/json
{
"errors": [
{
"status": 500,
"source": {
"pointer": "/test/internal-server-error"
},
"title": "Internal Server Error",
"detail": "500 INTERNAL_SERVER_ERROR"
}
]
}
Sample 6: Our customized Spring Boot error response as JSON:API Error Object.
Simple as that :-)
Bonus: JSON instead of Whitelabel Error Page
From the start Spring Boot offers an error controller to handle exceptions. This controller provides a JSON in the REST context and a HTML page (the Whitelabel Error Page) for browsers. We have already looked at how we can adapt the JSON to our needs (i.e. JSON:API Error Object). Now the question is: what do we do with the Whitelabel Error Page?
There are various ways to prevent a Whitelabel Error Page.
If we don’t want to see the Whitelabel Error Page, the server.error.whitelabel.enabled
property can be set to false
in the application.properties
.
server.error.whitelabel.enabled=false
Sample 7: application.properties
If the test endpoint http://localhost:8080/test/internal-server-error
is now called in the browser, we no longer receive HTML from Spring, but a standard HTML from the respective browser.
Another option in Spring Boot would be to create your own HTML page for each error. This is particularly useful for frontend services. But as this article is about the JSON:API Error Object we naturally want to see this JSON in the browser.
All we have to do is implement the error controller provided by Spring Boot. We create a class JsonErrorController.class
which extends the AbstractErrorController
and overrides the method getErrorPath()
. In this method we refer to the new endpoint /error
. In the controller itself, we simply take the ErrorAttributes
(which we changed above) and return them as a ResponseEntity
.
@RestController
@RequestMapping("/error")
public class JsonErrorController extends AbstractErrorController {
public JsonErrorController(final ErrorAttributes errorAttributes) {
super(errorAttributes);
}
@GetMapping
public ResponseEntity<Map<String, Object>> error(final HttpServletRequest request) {
final Map<String, Object> body = this.getErrorAttributes(request, false);
final HttpStatus status = this.getStatus(request);
return new ResponseEntity<>(body, status);
}
@Override
public String getErrorPath() {
return "/error";
}
}
Sample 8: JsonErrorController.java
As a result, we see the desired JSON:API Error Object not only in the console with curl, but also in the browser when we call the test endpoint http://localhost:8080/test/internal-server-error
.
Conclusio
We have overwritten Spring Boot’s DefaultErrorAttributes
to get the standard error message in the form of a JSON:API Error Object. To check our implementation we also implemented a test controller. Additionally, the output of the JSON:API Error Object was also used for the browser instead of a Whitelabel Error Page.
The code presented in this article can be downloaded here.