ProblemDetail (RFC 7807)
ProblemDetail is Spring Framework 6’s implementation of RFC 7807 (now RFC 9457), the IETF standard for HTTP error bodies. Instead of inventing your own error DTO, you return a well-known shape with the media type application/problem+json, which a growing number of clients and tools understand out of the box. This page covers enabling it, returning ProblemDetail directly, the ErrorResponseException helper, and customizing the body with extension properties.
The RFC 7807 shape
A problem detail body has five standard members, all optional except by convention:
| Member | Type | Meaning |
|---|---|---|
type | URI | Identifies the problem kind (defaults to about:blank) |
title | string | Short, human-readable summary of the type |
status | int | HTTP status code |
detail | string | Human-readable explanation specific to this occurrence |
instance | URI | Identifies this specific occurrence (often the request path) |
You may add any number of extension members (e.g. code, errors) alongside these.
Enabling problem details
Spring’s built-in MVC exceptions (validation, unreadable body, method not allowed, …) can be rendered as problem details automatically. Turn it on with one property:
spring.mvc.problemdetails.enabled=true
(For WebFlux the key is spring.webflux.problemdetails.enabled=true.) With this enabled, an unsupported method now returns:
Output:
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'DELETE' is not supported.",
"instance": "/api/products/7"
}
The response carries Content-Type: application/problem+json.
Note: Even without that property,
ResponseStatusExceptionand anyErrorResponseyou throw still produceProblemDetailbodies — the flag specifically controls the built-in MVC exception handlers.
Returning ProblemDetail from a handler
The simplest custom usage: build a ProblemDetail in an @ExceptionHandler and return it. Spring serializes it as application/problem+json.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex) {
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND, ex.getMessage());
pd.setTitle("Resource Not Found");
pd.setType(URI.create("https://api.example.com/problems/not-found"));
pd.setProperty("code", "RESOURCE_NOT_FOUND");
pd.setProperty("timestamp", Instant.now());
return pd;
}
}
Output:
{
"type": "https://api.example.com/problems/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "Product with id 999 was not found",
"instance": "/api/products/999",
"code": "RESOURCE_NOT_FOUND",
"timestamp": "2026-06-13T10:15:42.123Z"
}
instance is populated by Spring from the request path. Use setProperty(...) to attach extension members like code and timestamp.
Customizing properties
ProblemDetail exposes setters for the standard members and setProperty for extensions:
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.CONFLICT);
pd.setTitle("Duplicate Email");
pd.setDetail("Email " + email + " is already registered");
pd.setType(URI.create("https://api.example.com/problems/duplicate-email"));
pd.setInstance(URI.create("/api/users"));
pd.setProperty("code", "DUPLICATE_EMAIL");
pd.setProperty("field", "email");
For validation errors, attach the field failures as an extension member:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(f -> Map.of("field", f.getField(),
"message", String.valueOf(f.getDefaultMessage())))
.toList();
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, "Request validation failed");
pd.setTitle("Validation Failed");
pd.setProperty("code", "VALIDATION_FAILED");
pd.setProperty("errors", errors);
return pd;
}
Output:
{
"type": "about:blank",
"title": "Validation Failed",
"status": 400,
"detail": "Request validation failed",
"instance": "/api/products",
"code": "VALIDATION_FAILED",
"errors": [
{ "field": "name", "message": "must not be blank" },
{ "field": "price", "message": "must be greater than 0" }
]
}
ErrorResponseException
ProblemDetail is a plain data holder — it is not throwable. To throw a problem from deep in your code, use ErrorResponseException, which wraps a ProblemDetail and implements Spring’s ErrorResponse contract.
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.FORBIDDEN, "You may not modify another user's cart");
pd.setType(URI.create("https://api.example.com/problems/forbidden"));
pd.setProperty("code", "CART_FORBIDDEN");
throw new ErrorResponseException(HttpStatus.FORBIDDEN, pd, null);
This requires no @ExceptionHandler — Spring already knows how to render any ErrorResponse as application/problem+json. ResponseStatusException is itself a simpler ErrorResponse; reach for ErrorResponseException when you need to set type, title, or extension properties on the body.
| Throwable | Sets body fields? | Use when |
|---|---|---|
ResponseStatusException | status + detail only | Quick status with a message |
ErrorResponseException | full ProblemDetail | Need type/title/extensions inline |
| Custom exception + advice | full control | Reused, mapped centrally |
ProblemDetail vs a custom DTO
| Aspect | ProblemDetail (RFC 7807) | Custom ApiError DTO |
|---|---|---|
| Media type | application/problem+json | application/json |
| Standardized | Yes (IETF) | No |
| Field names | Fixed (type/title/…) | Your choice |
| Extensions | Via setProperty | Native fields |
| Tooling support | Broad, growing | None |
Tip: Choose
ProblemDetailfor public or partner-facing APIs where interoperability matters. A bespoke error DTO is fine for internal services where you control all clients.
Pitfalls
ProblemDetailcannot be thrown — wrap it inErrorResponseException(or return it from a handler).- Setting
spring.mvc.problemdetails.enabled=truechanges the built-in exception responses; review clients that parse the old default format first. - Extension property values must be Jackson-serializable; prefer simple maps, strings, and instants.