Problem Details (RFC 9457)
Use Spring's ProblemDetail to answer errors in the standard application/problem+json format, and switch it on for Spring MVC's own exceptions.
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Booking 42 not found");
problem.setTitle("Booking not found");
problem.setProperty("bookingId", 42);
spring.mvc.problemdetails.enabled=trueRFC 9457, which replaced RFC 7807, defines one JSON format for HTTP API errors, served as application/problem+json. Using it means clients, gateways and API tooling can read your errors without learning a format you invented. Spring Framework models it as ProblemDetail.
The fields
| Field | Meaning |
|---|---|
type | A URI naming the kind of problem. Absent means about:blank: nothing beyond the status code. |
title | A short summary of the type, the same every time. Spring defaults it to the status reason phrase. |
status | The HTTP status, repeated in the body. |
detail | A human-readable explanation of this occurrence. |
instance | A URI for this occurrence; Spring MVC fills in the request path when you leave it empty. |
Anything you add with setProperty appears as an extra top-level member, which is where machine-readable data such as a field list belongs. Unset fields are left out of the JSON.
Your exceptions and Spring's
Returning a ProblemDetail from an @ExceptionHandler is all your own exceptions need: Spring sets the response status from it and writes the problem content type. Spring MVC's exceptions already carry a ProblemDetail too, because they implement ErrorResponse, but by default they still go through Boot's /error endpoint and come out in Boot's older shape.
Setting spring.mvc.problemdetails.enabled=true makes Boot register an advice based on ResponseEntityExceptionHandler that renders those exceptions as Problem Details. If you write your own subclass of ResponseEntityExceptionHandler as an advice, Boot's backs off and yours does the same job, with room to customise.
The gaps people miss
- The property covers Spring MVC's exceptions and any
ErrorResponseException. An unhandledRuntimeExceptionfrom your code still reaches/error, so a client sees two different error formats. Add your own handlers, including one for unexpected exceptions. - Inside a
ResponseEntityExceptionHandlersubclass, customise Spring's exceptions by overriding methods such ashandleMethodArgumentNotValid. Declaring a second@ExceptionHandlerfor the same type fails at startup with an ambiguous mapping. - Keep
detailsafe to show a user. It is not a place for exception messages from libraries.
Pick one format for the whole API and test it, including the 500 path. Clients write their error handling once, against whatever your first error response looked like.
Example
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
class BookingNotFoundException extends RuntimeException {
BookingNotFoundException(long id) {
super("Booking " + id + " not found");
}
}
record FieldProblem(String field, String message) {}
public class ProblemDetailsAdvice extends ResponseEntityExceptionHandler {
// Your own exceptions: return a ProblemDetail.
ProblemDetail bookingNotFound(BookingNotFoundException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
}
// Spring's exceptions: override the protected method. Declaring another
// @ExceptionHandler for MethodArgumentNotValidException would clash with
// the base class at startup.
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatusCode status, WebRequest request) {
List<FieldProblem> errors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> new FieldProblem(error.getField(), error.getDefaultMessage()))
.toList();
ProblemDetail body = ex.getBody();
body.setDetail("The request has " + errors.size() + " invalid field(s).");
body.setProperty("errors", errors);
return handleExceptionInternal(ex, body, headers, status, request);
}
}
When to use it
- Give every error from a public API the same application/problem+json shape that API gateways understand.
- Return the list of invalid fields from a failed @Valid request body as a Problem Details extension.
- Make 405 and 415 responses from Spring MVC match the format of your own 404s.
More examples
Switching Problem Details on for Spring MVC
One line makes 405, 415, missing parameter and unreadable body errors come back as <code>application/problem+json</code>. It is off by default. Once you write your own <code>ResponseEntityExceptionHandler</code> advice, as in the main example, the property no longer matters because Boot's handler steps aside.
# Boot registers a ResponseEntityExceptionHandler advice for Spring MVC's own exceptions.
# It backs off if you declare your own ResponseEntityExceptionHandler bean.
spring.mvc.problemdetails.enabled=true
An exception that carries its own problem
Extending <code>ErrorResponseException</code> puts the status and body next to the condition that causes them. <code>ResponseEntityExceptionHandler</code> already handles <code>ErrorResponseException</code>, so no extra handler method is needed. The trade-off is that the exception now depends on Spring's web classes; keep this style for exceptions thrown from the web layer and map pure domain exceptions in an advice.
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.ErrorResponseException;
// An ErrorResponseException carries its own ProblemDetail. ResponseEntityExceptionHandler
// (yours, or the one spring.mvc.problemdetails.enabled registers) renders it as is.
public class InsufficientStockException extends ErrorResponseException {
public InsufficientStockException(String sku, int requested, int available) {
super(HttpStatus.CONFLICT, problem(sku, requested, available), null);
}
private static ProblemDetail problem(String sku, int requested, int available) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT,
"Only " + available + " of " + sku + " left, " + requested + " requested.");
problem.setTitle("Insufficient stock");
problem.setProperty("sku", sku);
problem.setProperty("available", available);
return problem;
}
}
A validation failure as Problem Details
This is what the overridden <code>handleMethodArgumentNotValid</code> produces for a booking with zero guests and a past check-in date. <code>type</code> is absent, which RFC 9457 reads as <code>about:blank</code>. The <code>errors</code> array is an extension member: a client can highlight each field without parsing the <code>detail</code> sentence.
{
"title": "Bad Request",
"status": 400,
"detail": "The request has 2 invalid field(s).",
"instance": "/api/bookings",
"errors": [
{ "field": "guests", "message": "must be greater than or equal to 1" },
{ "field": "checkIn", "message": "must be a future date" }
]
}
Discussion