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 | エラーの短い説明 |
status | HTTPステータスコード |
detail | 具体的な説明 |
instance | エラーが発生したリソースのURI |
Spring Boot 3.xでの有効化
Spring Boot 3.0から ProblemDetail クラスが標準搭載されています。まず application.properties に一行追加しましょう。
spring.mvc.problemdetails.enabled=true
これだけで、Spring MVCが処理する標準例外(NoHandlerFoundException や HttpMessageNotReadableException など)がすべて 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
既存の @ControllerAdvice を ResponseEntityExceptionHandler を継承する形に変更すると、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対応の記事 もあわせてどうぞ。