Skip to content
Spring Boot sb exceptions 4 min read

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:

MemberTypeMeaning
typeURIIdentifies the problem kind (defaults to about:blank)
titlestringShort, human-readable summary of the type
statusintHTTP status code
detailstringHuman-readable explanation specific to this occurrence
instanceURIIdentifies 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, ResponseStatusException and any ErrorResponse you throw still produce ProblemDetail bodies — 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.

ThrowableSets body fields?Use when
ResponseStatusExceptionstatus + detail onlyQuick status with a message
ErrorResponseExceptionfull ProblemDetailNeed type/title/extensions inline
Custom exception + advicefull controlReused, mapped centrally

ProblemDetail vs a custom DTO

AspectProblemDetail (RFC 7807)Custom ApiError DTO
Media typeapplication/problem+jsonapplication/json
StandardizedYes (IETF)No
Field namesFixed (type/title/…)Your choice
ExtensionsVia setPropertyNative fields
Tooling supportBroad, growingNone

Tip: Choose ProblemDetail for public or partner-facing APIs where interoperability matters. A bespoke error DTO is fine for internal services where you control all clients.

Pitfalls

  • ProblemDetail cannot be thrown — wrap it in ErrorResponseException (or return it from a handler).
  • Setting spring.mvc.problemdetails.enabled=true changes 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.
Last updated June 13, 2026
Was this helpful?