Validation and Exception Handling with Spring

Christoph Huber
sprang
Published in
5 min readApr 22, 2020

Whenever I start implementing a new REST API with Spring, I struggle with the decision of how to validate requests and to handle business exceptions. Unlike for other typical API problems, Spring and its community does not seem to agree on best practices for these problems and it is hard to find helpful articles about this.

In this article, I summarize my experiences and give some advices how to validate interfaces.

Architecture & Terminology

I build my applications that provide web APIs following the “Onion Architecture” pattern. This article is not about Onion architecture, but I would like to mention some of its key points that are important for understanding my thoughts:

  • REST Controllers and any web-related components and configurations are part of the outer “infrastructure” layer.
  • The middle “service” layer contains Services that aggregate business functions and solve cross-cutting concerns like security or transactions.
  • The inner “domain” layer contains the business logic without any infrastructure-related concerns like databases, web endpoints, etc.
A sketch of the Onion Architecture layers and where typical Spring classes are placed.

The architecture does allow dependencies from outer layers to inner, but not vice versa. For a REST endpoint, a request flow might look as follows:

  • The request is dispatched to a controller in the “infrastructure” layer.
  • The controller deserializes the request and — if successful — asks the appropriate service in the “service” layer for the result.
  • The service checks if the current user has permission to call the function and initializes a database transaction (if needed).
  • It then fetches data from “domain” repositories, manipulates it and maybe stores it back into the repository.
  • The service may also call several repositories, transform and aggregate the results.
  • The repository in the “domain” layer returns the business objects. This layer is responsible to keep all objects in a valid state.
  • Dependent on the service’s response, which is either a valid result or an exception, the “infrastructure” layer serializes the response.
Validation at request-level, service-level and domain-level.

In this architecture we have three interfaces, each requiring a different kind of validation:

  • The controller defines the first interface. In order to deserialize the request, the request needs to be validated against our API schema. This is done implicitly by a mapping framework like Jackson and explicitly by constraints like @NotNull. We call this request validation.
  • The service may check the privileges of the current user and ensure preconditions that will make calling the domain layer possible. Let us call this service validation.
  • While the previous validations ensure some basic preconditions, the domain layer alone is responsible for keeping a valid state. This domain validation is the most crucial one.

Request Validation

Normally we deserialize an incoming request, which is already an implicit validation against the request parameters and request body. Spring Boot auto-configures Jackson deserialization and general exception handling. For example, take a look at my BGG demo’s sample controller:

Both calling it with a missing parameter and a wrong type returns error messages with the correct status code:

With Spring Boot’s default configuration, we would get stacktraces too. I turned them off by setting

in the application.yml. This default error handling is provided by the BasicErrorController in classic Web MVC and by the DefaultErrorWebExceptionHandler in WebFlux, both retrieving the response body from ErrorAttributes.

Data Binding

The examples above demonstrate @RequestParam attributes, or any simple controller method attribute without annotation. Request validation becomes different when validating @ModelAttribute, @RequestBody or non-simple parameters like in

Where @RequestParam annotations can be used to make a parameter required or with default value, in command objects this is done with bean validation constraints like @NotNull and plain Java/Kotlin. To activate bean validation, the method argument has to be annotated with @Valid.

When the bean validation fails, a BindException or WebExchangeBindException in the reactive stack is thrown. Both exceptions implement BindingResult, which provides nested errors for each invalid field value. Above controller method would result in error messages like

Customizing exception handling

The response message above is not client-friendly, because it contains class names and other internal hints, which are not readable for an API client. An even worse example of the Spring Boot default exception handling is

This also returns the wrong error code, implicating a server error, even though the client provided the wrong type for “since”. Both examples were generated with the reactive stack, MVC has better defaults. For both we need to customize the exception handling. This can be done by providing an own ErrorAttributes bean, that writes the response body we want. The response status code is provided by the “status” value.

Or we can go for a smaller intervention and use the DefaultErrorAttributes implementation by either decorating exceptions with a @ResponseStatus annotation or letting all exceptions extend ResponseStatusException. Both ways allow customizing the response status and “message” value. Unfortunately most exceptions thrown in the infrastructure layer are provided by the framework and cannot be customized, so we need another solution. One possibility for annotated controllers is using @ExceptionHandler for individual exceptions. Then we could build a response from scratch, but this would skip the default exception handling and we would like to have the same handling for every exception. Thus, to improve the response above just rethrow the exceptions:

Summary

I wrote a lot about Spring Boot default configurations, which is always a good start in Spring in my opinion. On the other hand the default exception handling is rather complicated and you could start interfering at many levels, which are from top to bottom:

  • Directly in the Controller with try/catch (MVC) or onErrorResume() (Webflux). I do not recommend this in most cases, because a cross-cutting concern like exception handling should be defined globally to guarantee a consistent behavior.
  • Intercept exceptions in @ExceptionHandler functions. Create your own responses, with @ExceptionHandler(Throwable.class) for the default case.
  • Or rethrow the exceptions, annotate them with @ResponseStatus or extend ResponseStatusException to customize the response for certain cases.

I like to start Spring Boot applications with the default configuration and replace parts where needed. In this case I recommended starting with the third option, and if more customization is needed, switching to the second.

In this blog I only scratched the surface of all the learnings I collected through the years. There are a lot more topics around validation and exception handling like internalization of error messages, custom constraint annotations, difference between Java and Kotlin, auto-documenting constraints and of course — validating data in the inner layers. These topics are covered in my next article.

--

--