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で処理されます。バリデーションを @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が投げる AuthenticationException や AccessDeniedException はProblemDetail形式になりません。AuthenticationEntryPoint と 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);
}
}
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. title や detail は文字列なので、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対応の記事 もあわせてどうぞ。