When developing REST APIs, error response formats tend to vary across teams and projects. Sometimes it’s { "error": "Not Found" }, other times { "message": "...", "code": 404 }.

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

What is RFC 9457 (Problem Details)?

It’s a specification that defines the error response format for HTTP APIs. It’s the successor to RFC 7807, but there are virtually no substantive changes.

The biggest difference from custom formats is that Content-Type: application/problem+json is used. Clients can identify error responses by checking this Content-Type.

The main response fields are as follows.

FieldDescription
typeURI indicating the error type
titleShort description of the error
statusHTTP status code
detailConcrete explanation
instanceURI of the resource where the error occurred

Enabling It in Spring Boot 3.x

The ProblemDetail class has been included by default since Spring Boot 3.0. First, add one line to application.properties.

spring.mvc.problemdetails.enabled=true

That’s all it takes — all standard exceptions handled by Spring MVC (such as NoHandlerFoundException and HttpMessageNotReadableException) will now be returned in application/problem+json format.

Let’s compare 404 responses 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’s 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 one without a detail, while forStatusAndDetail(HttpStatus, String) lets you pass a detail message. Simple and intuitive.

@ControllerAdvice Extending ResponseEntityExceptionHandler

By changing your existing @ControllerAdvice to extend ResponseEntityExceptionHandler, you can make Spring MVC’s standard exceptions support ProblemDetail all 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);
    }
}

Just by extending ResponseEntityExceptionHandler, standard exceptions like MethodArgumentNotValidException are also automatically processed as ProblemDetail. If you’re implementing validation with @Valid, also check out How to use the Spring Boot @Valid annotation to get a complete picture of error handling.

Implementing ErrorResponse in a Custom Exception Class

By implementing the ErrorResponse interface in your exception class itself, the exception can carry 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 shape, ResponseEntityExceptionHandler automatically handles it without you having to write individual handlers in @ControllerAdvice.

Adding 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 will look like this.

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

Additional fields are treated as part of the contract with the client, so it’s a good idea to document them in your OpenAPI spec. If you want to integrate with a distributed tracing infrastructure and include traceId in error responses, Distributed Tracing with Micrometer Tracing and Zipkin in Spring Boot 3.2+ is also a useful reference.

Handling Validation Errors

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

{
  "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 appears in the errors array. If you want to customize it, override ResponseEntityExceptionHandler#handleMethodArgumentNotValid. If you need to go deeper into group-based validation or service-layer validation, see Group-based Validation with @Validated Annotation in Spring Boot.

Migration Patterns from Existing Formats

Verifying with curl/HTTPie

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

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

After enabling, application/problem+json will be returned as follows.

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 verifying 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 won’t convert AuthenticationException or AccessDeniedException thrown by Spring Security into ProblemDetail format. You need to implement your own 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);
    }
}

By registering them via http.exceptionHandling() in SecurityConfig, you can return authentication/authorization errors in a unified format too.

Design Guidelines for the type Field URI

type is originally meant to be “a URI to documentation explaining 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 a documentation page (HTML or Markdown) explaining the error at that URI
  • Maintain URI compatibility even across major version updates (clients may use it 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 basic API is shared, so handler logic written for MVC can largely be reused.

Common Errors and Troubleshooting

Format doesn’t change even after setting spring.mvc.problemdetails.enabled=true

It’s likely that your @ControllerAdvice handler is matching first. Either re-extend ResponseEntityExceptionHandler, or remove the conflicting handler.

Content-Type still returns as application/json

Returning the body directly with something like ResponseEntity.ok(problem) causes the default JSON converter to take precedence. It’s safer to be explicit with ResponseEntity.status(...).contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problem).

ProblemDetail not 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.

FAQ

Q. What’s the difference between RFC 9457 and RFC 7807?

A. RFC 9457 is a revision of RFC 7807 and maintains backward compatibility. From an implementation perspective, there’s no significant difference in field structure, and Spring Boot 3.x’s ProblemDetail satisfies both specs.

Q. Can ProblemDetail support multiple languages?

A. Since title and detail are strings, you can combine them with MessageSource to support multiple languages. For i18n methods for error messages, see Spring Boot Internationalization.

Q. Can it be used in Spring Boot 2.x?

A. The ProblemDetail class itself was added in Spring Framework 6.0 or later, so Spring Boot 3.x or higher is required. On 2.x, you have to build something similar using a custom format and custom response class. You may want to first check the Spring Boot 2.x to 3.x Migration Guide.

Q. Should the error code (errorCode) be a standard field?

A. Since it’s not included in RFC 9457’s standard fields, the recommended approach is to add it as an extension property with setProperty("errorCode", "..."). Document it explicitly in your OpenAPI spec as part of the client contract.

Q. Is it OK to use ProblemDetail directly for log output?

A. Since instance may contain paths derived from user input, sanitization is needed when logging. The same applies to detail — be careful not to include sensitive information.

When considering migrating an API that already uses a custom error format, there are mainly three approaches.

Gradual migration (recommended) Use ProblemDetail for new endpoints, and leave existing ones as-is for now. This minimizes the impact on clients and lets you proceed while keeping risk low.

Bulk migration Add spring.mvc.problemdetails.enabled=true and refactor @ControllerAdvice all at once. Suitable when you have few clients and are timing it with an API version overhaul.

Coexistence pattern Switch the format returned based on the presence of the Accept: application/problem+json header. The implementation cost is high, so unless there’s a strong reason, you can leave this off the table.

Summary

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

  • Just adding spring.mvc.problemdetails.enabled=true makes standard exceptions return RFC-compliant responses
  • ProblemDetail.forStatusAndDetail() lets you generate error responses simply
  • Extending ResponseEntityExceptionHandler enables ProblemDetail support for standard exceptions in bulk
  • Extending ErrorResponseException allows domain exceptions themselves to carry ProblemDetail
  • setProperty() enables flexible addition of extra fields

The easiest first step is to try spring.mvc.problemdetails.enabled=true. If you’re interested in internationalizing error messages, also check out the i18n article.