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で処理されます。バリデーションを @Valid で実装している場合は Spring Boot @Validアノテーションの使い方 もあわせて確認すると、エラーハンドリングの全体像が掴めます。

カスタム例外クラスに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ドキュメントに記載しておくと親切です。分散トレース基盤と連携してtraceIdをエラーレスポンスに含めたい場合は Spring Boot 3.2+でMicrometer TracingとZipkinを使う分散トレーシング も参考になります。

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

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 をオーバーライドしてください。グループ別バリデーションやService層の検証まで踏み込む場合は Spring Boot @Validatedアノテーションでグループ別バリデーション を参考にどうぞ。

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

curl/HTTPieで動作確認する

ProblemDetailが正しく返っているかは、Content-Type ヘッダで確認するのが確実です。

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

有効化後は以下のように application/problem+json が返ります。

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 が便利です。

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

Spring Securityの認証/認可エラーをProblemDetail化する

spring.mvc.problemdetails.enabled=true だけではSpring Securityが投げる AuthenticationExceptionAccessDeniedException はProblemDetail形式になりません。AuthenticationEntryPointAccessDeniedHandler を独自実装する必要があります。

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

SecurityConfigで http.exceptionHandling() 経由で登録すれば、認証/認可エラーも統一フォーマットで返せます。

typeフィールドのURI設計指針

type は本来「エラー種別を解説するドキュメントURI」です。about:blank のままでも仕様違反ではありませんが、API利用者向けにエラーカタログを用意するとデバッグ体験が大きく向上します。

推奨パターン

  • https://api.example.com/errors/user-not-found のように kebab-case でリソース+原因 を表現
  • 該当URIにエラーの解説ページ(HTMLでもMarkdownでも可)を実際に公開する
  • メジャーバージョン更新時もURIは互換性を保つ(クライアントが分岐に使う可能性があるため)

WebFluxでの差異

Spring WebFluxを使っている場合は spring.webflux.problemdetails.enabled=true を設定します(MVCと別プロパティ)。@ExceptionHandler の戻り値は Mono<ResponseEntity<ProblemDetail>> になります。基本的なAPIは共通なので、MVCで作ったハンドラのロジックはほぼ流用できます。

よくあるエラーとトラブルシューティング

spring.mvc.problemdetails.enabled=true を設定したのにフォーマットが変わらない

@ControllerAdvice のハンドラが先にマッチしている可能性が高いです。ResponseEntityExceptionHandler を継承し直すか、該当ハンドラを削除してください。

Content-Typeが application/json のまま返る

ResponseEntity.ok(problem) のように直接ボディを返すと、デフォルトのJSONコンバーターが優先されます。ResponseEntity.status(...).contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problem) と明示するのが安全です。

Spring Boot 2.7からの移行で ProblemDetail が見つからない

org.springframework.http.ProblemDetail はSpring Framework 6.0 (Spring Boot 3.0) 以降にしか存在しません。先にSpring Boot 3系へのアップグレードが必要です。

FAQ

Q. RFC 9457とRFC 7807の違いは?

A. RFC 9457はRFC 7807の改訂版で、後方互換性が保たれています。実装観点ではフィールド構造に大きな差はなく、Spring Boot 3.xのProblemDetailは両方の仕様を満たします。

Q. ProblemDetailで多言語対応はできる?

A. titledetail は文字列なので、MessageSource と組み合わせれば多言語化できます。エラーメッセージのi18n方法は Spring Bootの国際化対応 を参照してください。

Q. Spring Boot 2.xでも使える?

A. ProblemDetail クラス自体はSpring Framework 6.0以降で追加されたため、Spring Boot 3.x以上が前提です。2.xでは独自フォーマット+カスタムレスポンスクラスで近い構造を作るしかありません。先に Spring Boot 2.xから3.xへの移行ガイド を確認すると良いでしょう。

Q. エラーコード(errorCode)は標準フィールドにすべき?

A. RFC 9457の標準フィールドには含まれないため、setProperty("errorCode", "...") で拡張プロパティとして付与するのが推奨です。クライアントとの契約として OpenAPI に明記しましょう。

Q. ログ出力にProblemDetailをそのまま使ってよい?

A. instance にユーザー入力由来のパスが入る可能性があるため、ログ出力時はサニタイズが必要です。detail も同様で、機密情報を含めないようにしてください。

すでに独自エラーフォーマットを使っている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対応の記事 もあわせてどうぞ。