Back to Home

JSON error handling with Spring Boot

java spring

JSON error handling with Spring Boot

Often when working with microservices built using Spring Boot technology , you can see standard error output like this:

{
    "timestamp": 1510417124782,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "com.netflix.hystrix.exception.HystrixRuntimeException",
    "message": "ApplicationRepository#save(Application) failed and no fallback available.",
    "path": "/application"
}

Such a conclusion may be unnecessary and unnecessary customers of your service. If you want to simplify the life of third-party services in case of an error, then this will be discussed in this post.

We will start by building a small service with one controller. Our service will accept a request for a user and, if successful, give data to the user. In case of failure, an error is returned to us. Let's start with a simple and further in the article we will improve the project.

So, the first thing we need is a user:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String firstName;
    private String lastName;
}

Here I used the lombok library . Abstract Data substitutes getters and setters in a class. The remaining annotations add an empty constructor and a constructor with parameters. If you want to repeat this example in your IntelliJ Idea , then you need to check the enable annotation processing box, or write everything by hand.

Next, we need a service (for brevity, we will not create a repository):

import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class UserService {
    private static final Map userStorage
            = new HashMap<>();
    static {
        userStorage.put(1, new User(1, "Petr", "Petrov"));
        userStorage.put(2, new User(2, "Ivan", "Ivanov"));
        userStorage.put(3, new User(3, "Sergei", "Sidorov"));
    }
    public User get(int id) {
        return userStorage.get(id);
    }
}

And, of course, the controller itself:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("user")
public class UserController {
    private UserService userService;
    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;
    }
    @GetMapping("{id}")
    public User get(@PathVariable(name = "id") int id) {
        return userService.get(id);
    }
}

So, we have an almost complete service with users. We launch it and look.

When we request the localhost : 8080 / user / 1 URL, we get json in this format:

{
    "id": 1,
    "firstName": "Petr",
    "lastName": "Petrov"
}

Everything is fine. But what happens if we make a request to the localhost URL : 8080 / user / 4 (we have only 3 users)? The correct answer: we will receive the status of 200 and nothing in the answer. The situation is not very pleasant. There is no error, but there is no requested object either. Let's improve our service and add to it throwing errors in case of failure. First, create an exception:



public class ThereIsNoSuchUserException extends RuntimeException { }

Now add the error throwing to the service:

public User get(int id) {
    User user = userStorage.get(id);
    if (user == null) {
        throw new ThereIsNoSuchUserException();
    }
    return user;
}

We will restart the service and see again what happens when a non-existent user is requested:

{
    "timestamp": 1510479979781,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
    "message": "No message available",
    "path": "/user/4"
}

That's better. It is much more informative and the status code is not 200. The client, on his side, will be able to successfully and easily handle such a situation. But, as they say, there is one caveat. Errors can be completely different, and the client of our service will have to put a bunch of conditional statements and investigate what went wrong with us and how this can be fixed. It turns out a little rude on our part.

Just for such cases, the ResponseStatus annotation was invented. We substitute it in the place of our exclusion and in practice see how it works:

@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "There is no such user")
public class ThereIsNoSuchUserException extends RuntimeException {
}

Repeat the query and see the result:

{
    "timestamp": 1510480307384,
    "status": 404,
    "error": "Not Found",
    "exception": "org.faoxis.habrexception.ThereIsNoSuchUserException",
    "message": "There is no such user",
    "path": "/user/4"
}

Excellent! The status code and message have changed. Now the client will be able to determine the reason for the error by the response code and even clarify it by the message field. But still there is a problem. The client may simply not need most of the fields. For example, the response code as a separate field may be redundant, since we get it with the response code anyway. Something needs to be done with this.

Fortunately, with spring boot, the last step to our successful error notification is not that difficult.

All that is required is to parse a couple of annotations and one class:

  • Annotation ExceptionHandler . Used to handle your own and some specific exceptions. Further in the example, it will be understood what this means. Just in case, a link to the documentation.
  • ControllerAdvice annotation . This annotation gives “advice” to a group of controllers on certain events. In our case, this is error handling. By default it is applied to all controllers, but in the parameters you can specify a distributed group. More here .
  • Class ResponseEntityExceptionHandler . This class handles errors. It has a bunch of methods whose name is built on the principle of handle + the name of the exception. If we want to handle some basic exception, we inherit from this class and redefine the required method.

Let’s now see how to combine all this and build our unique error message:

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class AwesomeExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(ThereIsNoSuchUserException.class)
    protected ResponseEntity handleThereIsNoSuchUserException() {
        return new ResponseEntity<>(new AwesomeException("There is no such user"), HttpStatus.NOT_FOUND);
    }
    @Data
    @AllArgsConstructor
    private static class AwesomeException {
        private String message;
    }
}


Let's do the same request and see the status of 404 response and our message with a single field:

{
    "message": "There is no such user"
}

The ResponseStatus annotation over our exception can be safely removed.

As a result, we have an application in which error handling is configured as flexibly and simply as possible. The full project can be found in the github repository . I hope that everything was simple and clear. Thank you and write comments! I will be glad to your comments and clarifications!

Read Next