When developing REST APIs, error response formats tend to vary from team to team and project to project. Sometimes it’s { "error": "Not Found" }, sometimes { "message": "...", "code": 404 }.

Spring Boot 3.x adds standard support for RFC 9457 (Problem Details for HTTP APIs), letting you switch to a unified error response format with a single configuration setting. Assuming you already have an existing @ControllerAdvice-based implementation, let’s walk through how to use it in practice.

What Is RFC 9457 (Problem Details)?

RFC 9457 is a specification that defines an error response format for HTTP APIs. It is the successor to RFC 7807 and maintains backward compatibility, so from an implementation standpoint there is little difference in field structure. Spring Boot 3.x’s ProblemDetail satisfies both specifications.

The biggest difference from a custom format is that Content-Type: application/problem+json is used. Clients can look at this Content-Type to recognize the response as an error.

The main fields in the response are as follows.

FieldDescription
typeA URI identifying the error type
titleA short description of the error
statusThe HTTP status code
detailA specific explanation
instanceThe URI of the resource where the error occurred

Enabling It in Spring Boot 3.x

The ProblemDetail class has shipped with Spring Boot since version 3.0. First, add one line to application.properties.

spring.mvc.problemdetails.enabled=true

With just this, all standard exceptions handled by Spring MVC (such as NoHandlerFoundException and HttpMessageNotReadableException) are returned in application/problem+json format.

Let’s compare a 404 response before and after enabling it.

Before enabling (Spring Boot default)

{
  "timestamp": "2026-04-18T10:00:00.000+00:00",
  "status": 404,
  "error": "Not Found",
  "path": "/api/users/999"
}

After enabling (RFC 9457 compliant)

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "No static resource api/users/999.",
  "instance": "/api/users/999"
}

Basic Usage of the ProblemDetail Class

Here is the basic pattern of creating and returning a ProblemDetail inside an @ExceptionHandler.

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ProblemDetail> handleUserNotFound(
        UserNotFoundException ex, HttpServletRequest request) {
    ProblemDetail problem = ProblemDetail.forStatusAndDetail(
        HttpStatus.NOT_FOUND, ex.getMessage()
    );
    problem.setTitle("ユーザーが見つかりません");
    problem.setType(URI.create("https://api.example.com/errors/user-not-found"));
    problem.setInstance(URI.create(request.getRequestURI()));
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
}

ProblemDetail.forStatus(HttpStatus) creates an instance without a detail, while forStatusAndDetail(HttpStatus, String) lets you pass a detail message. Simple and intuitive.

Since title and detail are plain strings, you can combine them with MessageSource to support multiple languages. For how to internationalize error messages, see Internationalization in Spring Boot.

A @ControllerAdvice That Extends ResponseEntityExceptionHandler

If you change your existing @ControllerAdvice to extend ResponseEntityExceptionHandler, you can make all of Spring MVC’s standard exceptions ProblemDetail-aware at once.

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ProblemDetail> handleUserNotFound(
            UserNotFoundException ex, HttpServletRequest request) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND, ex.getMessage()
        );
        problem.setTitle("ユーザーが見つかりません");
        problem.setInstance(URI.create(request.getRequestURI()));
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problem);
    }
}

Simply by extending ResponseEntityExceptionHandler, standard exceptions such as MethodArgumentNotValidException are automatically handled as ProblemDetail as well. If you implement validation with @Valid, also check out How to Use the @Valid Annotation in Spring Boot to get the full picture of error handling.

Implementing ErrorResponse on Custom Exception Classes

If you have the exception class itself implement the ErrorResponse interface, the exception can carry the ProblemDetail information directly. The simplest approach is to extend ErrorResponseException.

public class UserNotFoundException extends ErrorResponseException {

    public UserNotFoundException(Long userId) {
        super(HttpStatus.NOT_FOUND, createProblemDetail(userId), null);
    }

    private static ProblemDetail createProblemDetail(Long userId) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND,
            "ユーザーID " + userId + " は存在しません"
        );
        problem.setTitle("ユーザーが見つかりません");
        problem.setType(URI.create("https://api.example.com/errors/user-not-found"));
        return problem;
    }
}

With this structure, ResponseEntityExceptionHandler handles the exception automatically without you having to write an individual handler in @ControllerAdvice.

Adding Extra Properties with Extensions

RFC 9457 allows you to add properties beyond the standard fields. Use setProperty().

ProblemDetail problem = ProblemDetail.forStatusAndDetail(
    HttpStatus.INTERNAL_SERVER_ERROR, "予期しないエラーが発生しました"
);
problem.setProperty("errorCode", "SYS-001");
problem.setProperty("traceId", MDC.get("traceId"));

The response looks like this.

{
  "type": "about:blank",
  "title": "Internal Server Error",
  "status": 500,
  "detail": "予期しないエラーが発生しました",
  "instance": "/api/orders",
  "errorCode": "SYS-001",
  "traceId": "abc123def456"
}

Custom codes like errorCode are not part of the standard RFC 9457 fields, so the recommended approach is to attach them as extension properties via setProperty() as shown here. Additional fields become part of your contract with clients, so it’s helpful to document them in your OpenAPI specification. Note that instance may contain a path derived from user input, so sanitize it if you log ProblemDetail objects, and take care not to include sensitive information in detail either. If you want to integrate with a distributed tracing platform and include a traceId in error responses, Distributed Tracing with Micrometer Tracing and Zipkin in Spring Boot 3.2+ is also a useful reference.

Handling Validation Errors

Once you set spring.mvc.problemdetails.enabled=true, validation errors (MethodArgumentNotValidException) are automatically returned as ProblemDetail too.

{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid request content.",
  "instance": "/api/users",
  "errors": [
    {
      "object": "createUserRequest",
      "field": "email",
      "rejectedValue": "invalid-email",
      "message": "must be a well-formed email address"
    }
  ]
}

The list of field errors goes into the errors array. If you want to customize this, override ResponseEntityExceptionHandler#handleMethodArgumentNotValid. If you want to go further into group-based validation or validation in the Service layer, see Group-Based Validation with the @Validated Annotation in Spring Boot.

Verifying with curl/HTTPie

The most reliable way to confirm that ProblemDetail is being returned correctly is to check the Content-Type header.

curl -i http://localhost:8080/api/users/999

After enabling, you’ll get application/problem+json as shown below.

HTTP/1.1 404
Content-Type: application/problem+json

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "No static resource api/users/999.",
  "instance": "/api/users/999"
}

HTTPie is handy for checking validation errors.

http POST localhost:8080/api/users email=invalid-email

Converting Spring Security Authentication/Authorization Errors to ProblemDetail

Setting spring.mvc.problemdetails.enabled=true alone does not convert the AuthenticationException and AccessDeniedException thrown by Spring Security into ProblemDetail format. You need to provide your own implementations of AuthenticationEntryPoint and AccessDeniedHandler.

@Component
public class ProblemDetailAuthEntryPoint implements AuthenticationEntryPoint {
    private final ObjectMapper objectMapper;

    public ProblemDetailAuthEntryPoint(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException ex) throws IOException {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(
            HttpStatus.UNAUTHORIZED, "認証が必要です"
        );
        problem.setTitle("Unauthorized");
        problem.setInstance(URI.create(request.getRequestURI()));

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/problem+json");
        objectMapper.writeValue(response.getOutputStream(), problem);
    }
}

Register it via http.exceptionHandling() in your SecurityConfig, and authentication/authorization errors will be returned in the same unified format.

URI Design Guidelines for the type Field

The type field is meant to be a “URI pointing to documentation that explains the error type.” Leaving it as about:blank doesn’t violate the spec, but providing an error catalog for API consumers greatly improves the debugging experience.

Recommended patterns

  • Express resource + cause in kebab-case, like https://api.example.com/errors/user-not-found
  • Actually publish an explanation page (HTML or Markdown is fine) at that URI
  • Keep URIs compatible across major version updates (clients may use them for branching logic)

Differences in WebFlux

If you’re using Spring WebFlux, set spring.webflux.problemdetails.enabled=true (a separate property from MVC). The return type of @ExceptionHandler becomes Mono<ResponseEntity<ProblemDetail>>. The core API is the same, so the handler logic you wrote for MVC can be reused almost as-is.

Things to Check When It Doesn’t Work

The format doesn’t change even though spring.mvc.problemdetails.enabled=true is set

Most likely a handler in your @ControllerAdvice is matching first. Either change it to extend ResponseEntityExceptionHandler, or remove the handler in question.

The Content-Type is still returned as application/json

If you return the body directly, as in ResponseEntity.ok(problem), the default JSON converter takes precedence. It’s safer to be explicit with ResponseEntity.status(...).contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problem).

ProblemDetail can’t be found when migrating from Spring Boot 2.7

org.springframework.http.ProblemDetail only exists in Spring Framework 6.0 (Spring Boot 3.0) and later. You need to upgrade to Spring Boot 3.x first. The steps are summarized in the Spring Boot 2.x to 3.x Migration Guide.

Migration Patterns from an Existing Format

If you’re considering migrating an API that already uses a custom error format, there are three main approaches.

Gradual migration (recommended) Use ProblemDetail for new endpoints and leave existing ones as they are for now. This has minimal impact on clients and lets you proceed while keeping risk low.

All-at-once migration Add spring.mvc.problemdetails.enabled=true and refactor your @ControllerAdvice in one go. This suits situations where you have few clients and are rolling out a new API version anyway.

Coexistence pattern Switch the returned format based on the presence of an Accept: application/problem+json header. The implementation cost is high, so unless you have a compelling reason, you can safely drop this option.

Summary

Here are the key points for Problem Details support in Spring Boot 3.x.

  • Just adding spring.mvc.problemdetails.enabled=true turns standard exceptions into RFC-compliant responses
  • ProblemDetail.forStatusAndDetail() lets you generate error responses simply
  • Extending ResponseEntityExceptionHandler makes standard exceptions ProblemDetail-aware all at once
  • Extending ErrorResponseException lets domain exceptions themselves carry a ProblemDetail
  • setProperty() lets you flexibly attach additional fields

Trying out spring.mvc.problemdetails.enabled=true is an easy first step. If you’re interested in internationalizing error messages, also check out the i18n article.