REST APIを開発していると、エラーレスポンスのフォーマットってチームやプロジェクトごとにバラバラになりがちですよね。{ "error": "Not Found" } だったり { "message": "...", "code": 404 } だったり。

Spring Boot 3.xでは RFC 9457(Problem Details for HTTP APIs) が標準サポートされ、設定ひとつで統一されたエラーレスポンスに切り替えられるようになりました。既存の @ControllerAdvice ベースの実装がある前提で、具体的な使い方を見ていきましょう。

RFC 9457(Problem Details)とは

HTTP APIのエラーレスポンスフォーマットを定めた仕様です。RFC 7807の後継ですが、実質的な変更はほぼありません。

独自フォーマットとの一番の違いは Content-Type: application/problem+json が使われる点です。クライアントはこのContent-Typeを見てエラーレスポンスと判断できます。

レスポンスの主なフィールドはこちらです。

フィールド説明
typeエラー種別を示すURI
titleエラーの短い説明
statusHTTPステータスコード
detail具体的な説明
instanceエラーが発生したリソースのURI

Spring Boot 3.xでの有効化

Spring Boot 3.0から ProblemDetail クラスが標準搭載されています。まず application.properties に一行追加しましょう。

spring.mvc.problemdetails.enabled=true

これだけで、Spring MVCが処理する標準例外(NoHandlerFoundExceptionHttpMessageNotReadableException など)がすべて application/problem+json 形式で返るようになります。

有効化前後の404レスポンスを比べてみます。

有効化前(Spring Bootデフォルト)

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

有効化後(RFC 9457準拠)

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

ProblemDetailクラスの基本的な使い方

@ExceptionHandler の中で ProblemDetail を生成して返す基本パターンです。

@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) はdetailなし、forStatusAndDetail(HttpStatus, String) でdetailメッセージを渡せます。シンプルで直感的ですよね。

ResponseEntityExceptionHandlerを継承した@ControllerAdvice

既存の @ControllerAdviceResponseEntityExceptionHandler を継承する形に変更すると、Spring MVCの標準例外を一括でProblemDetail対応にできます。

@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);
    }
}

ResponseEntityExceptionHandler を継承するだけで、MethodArgumentNotValidException などの標準例外も自動的にProblemDetailで処理されます。既存の @ControllerAdvice との違いや詳しいエラーハンドリングの基礎については Spring Boot REST APIのエラーハンドリング もあわせて参照してください。

カスタム例外クラスにErrorResponseを実装する

例外クラス自体に ErrorResponse インターフェースを実装させると、例外がProblemDetailの情報を直接持てます。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;
    }
}

この形にしておくと、@ControllerAdvice に個別のハンドラを書かなくても ResponseEntityExceptionHandler が自動で処理してくれます。

extensionsで追加プロパティを付与する

RFC 9457では標準フィールド以外のプロパティも追加できます。setProperty() を使いましょう。

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

レスポンスはこのようになります。

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

追加フィールドはクライアントとの仕様として扱われるので、OpenAPIドキュメントに記載しておくと親切です。SwaggerUI連携の設定方法は OpenAPI/Swagger連携の記事 をご覧ください。

バリデーションエラーの対応

spring.mvc.problemdetails.enabled=true を設定しておくと、バリデーションエラー(MethodArgumentNotValidException)も自動的に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"
    }
  ]
}

フィールドエラーの一覧が errors 配列に入ります。カスタマイズしたい場合は ResponseEntityExceptionHandler#handleMethodArgumentNotValid をオーバーライドしてください。バリデーション実装の詳細は @Validアノテーションの記事 も参考になります。

既存フォーマットからの移行パターン

すでに独自エラーフォーマットを使っているAPIで移行を検討する場合、主に3つのアプローチがあります。

段階移行(おすすめ) 新規エンドポイントはProblemDetailを使い、既存は当面そのままにする。クライアントへの影響が最小限で、リスクを抑えながら進められます。

一括移行 spring.mvc.problemdetails.enabled=true を追加して @ControllerAdvice をまとめてリファクタリングする。クライアント数が少なく、APIバージョンを一新するタイミングに向いています。

共存パターン Accept: application/problem+json ヘッダの有無で返すフォーマットを切り替える。実装コストが高いので、よほど理由がない限り選択肢から外してよいでしょう。

まとめ

Spring Boot 3.xでのProblem Details対応のポイントをまとめます。

  • spring.mvc.problemdetails.enabled=true を追加するだけで標準例外がRFC準拠レスポンスになる
  • ProblemDetail.forStatusAndDetail() でシンプルにエラーレスポンスを生成できる
  • ResponseEntityExceptionHandler を継承すれば標準例外を一括でProblemDetail対応にできる
  • ErrorResponseException を継承するとドメイン例外自体がProblemDetailを持てる
  • setProperty() で追加フィールドも柔軟に付与できる

まずは spring.mvc.problemdetails.enabled=true を試してみるのが手軽な第一歩です。エラーメッセージの多言語化に興味があれば i18n対応の記事 もあわせてどうぞ。