JSON:API Error Object with Spring Boot REST API

Alexander
#spring boot  #java  #json api  #error  #whitelabel page 

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?

Screenshot from 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.

Screenshot from Browser Error Page

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.

Screenshot from Browser Error JSON

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.


Alexander

Alexander likes to do everything that can be done on foot: hiking, walking … or even running after the kids. If his family doesn’t keep him busy, the Java programmer volunteers for the fire and rescue service. Alex finds relaxation in strategy games, reading and listening to podcasts and as it should be IT stuff.


We're a team of makers, thinkers, organisers and digital explorers and we're always on the lookout for talented people.

Are you a good fit?