例外を共通のハンドラーで受け取り、入力チェックも実装できたら、次に整理したいのがAPIエラーの返却形式です。エンドポイントごとにJSONの項目が異なると、クライアント側にも個別の分岐が増えてしまいます。
Spring Bootでは、Spring FrameworkのProblemDetailを使って、RFC 9457に沿ったエラー本文を組み立てられます。この記事では例外処理と入力検証の基本を前提に、標準メンバーの意味、組み込み例外と独自例外の役割分担、機械向けの拡張情報を整理します。
ProblemDetailでAPIエラーをRFC 9457形式にそろえる
独自エラーDTOから共通の表現形式へ
独自DTOでcodeとmessageを返す方法でも、アプリ内の形式は統一できます。ただし、それぞれの項目が何を意味するかは、そのAPI独自の契約になります。
RFC 9457は、HTTP APIの問題を伝えるための共通の表現形式を定めています。従来のRFC 7807を置き換えた仕様で、問題の種類や説明を共通のメンバーで表し、API固有の情報は拡張できます。仕様の根拠はRFC 9457です。
独自DTOから移す際は、項目名を機械的に置き換えるのではなく、情報の役割で対応させます。たとえば、人が読むmessageはdetailへ、問題種別の識別はtypeへ、追加の機械向けコードが必要なら拡張メンバーのcodeへ整理します。
application/problem+jsonと主要5メンバー
Problem detailsをJSONで返すときのメディアタイプはapplication/problem+jsonです。主要なメンバーには、次の五つがあります。
| メンバー | 役割 | JSON上の型 |
|---|---|---|
| type | 問題種別を識別するURI参照 | 文字列 |
| title | 問題種別の短い説明 | 文字列 |
| status | この問題に対応するHTTPステータス | 数値 |
| detail | 今回発生した問題の利用者向け説明 | 文字列 |
| instance | 今回の問題の発生を識別するURI参照 | 文字列 |
五つすべてが常に必須という意味ではありません。たとえばtypeを省略した場合は、about:blankとして扱われます。どの情報を返すかは、それぞれの意味を踏まえて決めます。
次は、取得対象が見つからない場合の本文例です。https://api.example.com/problems/user-not-foundは、API提供者が管理する問題種別URIを置く位置を示しています。
{
"type": "https://api.example.com/problems/user-not-found",
"title": "ユーザーが見つかりません",
"status": 404,
"detail": "指定されたユーザーは見つかりません。",
"instance": "/api/users/123"
}type・title・status・detail・instanceの設計
typeで問題種別を識別し、titleで短く説明する
typeは、クライアントが「どの種類の問題か」を識別するためのURIです。同じ問題種別には同じURIを使い、個別のユーザーIDや例外メッセージを埋め込まないようにします。HTTPで参照できるURIを使う場合は、その種別の意味や対処方法を説明する文書につなげると理解しやすくなります。
titleは、その種別を短く説明する文言です。「ユーザーが見つかりません」のように種別の概要を表し、今回だけの事情はdetailに分けます。クライアントの分岐条件には、表示文言のtitleではなくtypeなどの識別値を使います。
HTTPステータス以上の意味を定義しない場合は、about:blankを使えます。この場合のtitleは、Not Foundなど、そのHTTPステータスの標準的な説明に合わせるのが基本です。業務上異なる問題をクライアントが区別する必要があるなら、固有のtypeを定義します。
statusをHTTPステータスと一致させる
statusを本文に含める場合は、実際に返すHTTPステータスと一致させます。本文が404なのにHTTP応答が200 OKでは、クライアントや監視の判断が食い違います。
Spring MVCでProblemDetailを直接返す場合、そのstatusがHTTP応答のステータスになります。後述の例ではProblemDetail.forStatusAndDetail(...)で設定し、本文とHTTPステータスを同じ値から組み立てます。
本文のstatusは、HTTPステータスを置き換える仕組みではありません。APIの契約では、HTTP応答そのものと本文の両方を整合させます。
detailは利用者向け説明、instanceは問題の発生を識別するURI
detailには、利用者が今回の問題を理解するための説明を書きます。「入力内容を確認してください」「同じキーで異なる入力は送信できません」など、公開してよい情報に絞ります。例外のgetMessage()を無条件に入れると、内部情報が混ざるおそれがあります。
instanceは、問題種別ではなく、今回の問題の発生を識別するURIです。Spring MVCでは、未設定の場合に現在のURL pathが設定されます。先ほどの/api/users/123は、この振る舞いに沿った例です。
同じパスで複数回エラーが起きることもあるため、URL pathだけで個々の発生を一意に区別できるとは限りません。問い合わせ対応で発生単位の照合が必要なら、公開可能な識別子を使ったinstanceや、別の拡張メンバーを設計します。URIにしただけで、閲覧可能な照会APIが自動的に作られるわけではありません。
Spring MVCでのステータス、instance、JSON変換の扱いは、Spring Framework 6.2のError Responsesに説明されています。
Spring Bootの自動構成と独自ハンドラーを使い分ける
spring.mvc.problemdetails.enabledで組み込み例外に対応する
Spring Boot 3.5では、次の設定により、Spring MVCの組み込み例外向けにResponseEntityExceptionHandlerが自動構成されます。application.propertiesでは次のように指定します。
spring.mvc.problemdetails.enabled=true設定の対象と役割は、Spring Boot 3.5のServlet Web Applicationsにあるエラー処理の説明で確認できます。
この設定は、独自の業務例外の意味や公開メッセージまで決めるものではありません。組み込み例外への対応を土台にし、アプリ固有の例外は独自のAdviceで変換します。
設定を有効にしたことと、全経路のエラー形式が統一されたことは別です。既存の独自ハンドラーやControllerから直接返すエラー、MVCの例外処理とは異なる経路も含め、APIの入口ごとに確認します。
独自例外を@RestControllerAdviceでProblemDetailへ変換する
独自例外では、例外の種類から公開する問題種別と説明を決めます。次は、ユーザーが見つからないことを表す例外の例です。各コードは同じパッケージの別ファイルに配置します。
package com.example.demo.api;
public class UserNotFoundException extends RuntimeException {
}AdviceではProblemDetailを作り、そのまま返します。Serviceなどからこの例外が送出されたときの変換部分に絞った例です。
package com.example.demo.api;
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"指定されたユーザーは見つかりません。"
);
problem.setType(URI.create(
"https://api.example.com/problems/user-not-found"
));
problem.setTitle("ユーザーが見つかりません");
return problem;
}
}この例では例外メッセージを返さず、ハンドラーで決めた文言を使っています。instanceは明示的に設定せず、Spring MVCが現在のURL pathを補う形です。問題種別URIは、実際のAPIで管理するURIに合わせます。
@ExceptionHandlerと@ControllerAdviceの基本や、Serviceから例外を送出する流れは、次の記事で補えます。

ProblemDetail・ErrorResponse・ResponseEntityExceptionHandlerの役割
名前が似た型もあるため、次のように役割を分けると整理しやすくなります。
| 型 | 役割 |
|---|---|
| ProblemDetail | RFC 9457形式の本文を表す |
| org.springframework.web.ErrorResponse | HTTPステータス、ヘッダー、ProblemDetailの本文を公開するSpringのインターフェース |
| ResponseEntityExceptionHandler | Spring MVCの例外を共通処理し、必要に応じて応答をカスタマイズするための基底クラス |
Spring MVCでは、@ExceptionHandlerや@RequestMappingのメソッドからProblemDetailまたはSpringのErrorResponseを返せます。先ほどの独自例外の例は、本文を直接作るためProblemDetailを選んでいます。
既存の例外処理記事に登場する独自DTOのErrorResponseと、Springのorg.springframework.web.ErrorResponseは別の型です。移行時はクラス名だけで判断せず、パッケージと役割を確認してください。
組み込み例外の応答も独自に調整したい場合は、ResponseEntityExceptionHandlerを継承する構成が選択肢になります。既存Adviceと自動構成を併用する場合は、同じ例外をどこが担当するかを整理し、意図した処理経路になることを確認します。
拡張メンバーで機械向けのエラー情報を表す
detailを解析させず、必要なエラーコードを定義する
「見つかりません」という文字列が含まれるかどうかでクライアントを分岐させると、文言変更や多言語化で処理が壊れます。detailは人に説明するためのメンバーなので、プログラムで解析させない設計にします。
機械向け情報は、typeや拡張メンバーで表します。たとえば既存クライアントが短い識別値を必要とするなら、codeを追加できます。先ほどのproblemへ追加する処理は次の形です。
problem.setProperty("code", "USER_NOT_FOUND");JacksonによるJSON変換では、ProblemDetail.propertiesの項目はトップレベルへ展開されます。JSONはpropertiesという入れ物で囲まれず、typeなどと同じ階層にcodeが並びます。
codeはRFCが必須としている名前ではなく、本記事の設計例です。typeだけで必要な分岐ができるなら、同じ意味のコードを追加する必要はありません。両方を持つ場合は対応関係を文書化し、どちらをクライアントの分岐基準にするかを決めます。
入力エラーの項目情報をerrorsで返す設計例
入力エラーでは、全体の説明に加えて「どの項目を直すか」が必要です。次は、全体をdetailで説明し、項目ごとの情報を拡張メンバーのerrorsへまとめる例です。
{
"type": "https://api.example.com/problems/validation-error",
"title": "入力内容に誤りがあります",
"status": 400,
"detail": "入力項目を確認してください。",
"instance": "/api/users",
"errors": [
{
"field": "name",
"code": "REQUIRED",
"message": "名前を入力してください。"
},
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "メールアドレスの形式を確認してください。"
}
]
}errorsという名前、配列の構造、field・code・messageは、このAPIで定める契約です。RFC 9457がこの形を必須にしているわけではありません。
Java側では、公開する項目だけを持つ型を用意し、setPropertyで追加できます。次は変換後の情報を格納する部分です。
public record InputError(String field, String code, String message) {
}problem.setProperty("errors", java.util.List.of(
new InputError("name", "REQUIRED", "名前を入力してください。"),
new InputError("email", "INVALID_FORMAT",
"メールアドレスの形式を確認してください。")
));検証結果から変換するときは、内部のフィールド名をそのまま公開せず、APIで使う項目名へ対応させます。制約名やメッセージも公開用の値へ変換し、拒否された入力値を無条件に含めないようにします。
さらに、一つの項目に複数のエラーを返すか、ネストした項目や配列をどう指定するか、複数項目にまたがるエラーをどう表すかも決めます。getFieldErrors()だけを扱う設計では、項目単位ではない検証結果を取りこぼす可能性があります。
この構造は、設定を有効にするだけで自動的に追加されるものではありません。検証結果を受け取る処理で、公開用のerrorsへ変換します。@ValidやBindingResultによる入力検証の基本は、次の記事を参照してください。

統一したエラー応答で確認すること
HTTPステータス・Content-Type・JSON項目を照合する
確認は、Javaのオブジェクトだけで終わらせず、HTTP応答として行います。少なくとも次の観点をそろえます。
| 確認対象 | 確認すること |
|---|---|
| HTTPステータス | 問題の意味に合い、本文のstatusと一致する |
| Content-Type | Problem detailsのJSONとしてapplication/problem+jsonで返る |
| 標準メンバー | typeが種別を表し、titleとdetailの役割が分かれている |
| instance | 意図したURIになり、公開できない情報を含まない |
| 拡張メンバー | codeやerrorsの型・構造が契約どおりで、トップレベルに出力される |
ケースには、独自例外だけでなく、不正なJSON、入力検証エラー、未対応のHTTPメソッドなど、実際に提供するAPIの組み込みエラーも含めます。既存ハンドラーが独自JSONを返す場合や、Controllerが直接エラーを返す場合も対象です。
クライアントが送るAcceptヘッダーも実際の利用条件に合わせます。本文の文字列全体を固定するより、ステータス、メディアタイプ、識別値、項目の型といった契約を確認すると、表示文言の変更と処理仕様の変更を分けて扱えます。
スタックトレース・内部情報・個人情報を返さない
Problem detailsは、実装のデバッグ情報を配布するための形式ではありません。RFC 9457でも、実装の内部情報などを公開するリスクへの配慮が求められています。
返却前には、detailだけでなく拡張メンバーも確認します。スタックトレース、SQL、内部ホスト名、認証情報、個人情報、入力本文全体を含めないようにします。検証エラーのメッセージにも入力値が埋め込まれていないかを確認してください。
ProblemDetailへ変換するだけで、情報漏えいが防げるわけではありません。例外メッセージや検証結果を丸ごと渡さず、外部へ公開する項目と文言を選んで組み立てます。
調査に必要な詳細は、アクセス制限や保存方針を設けたサーバー側の記録で管理します。レスポンスと記録を照合する識別子を用意する場合も、その識別子に秘密情報を含めず、識別子を知るだけで内部情報を閲覧できる構成にしないことが必要です。
既存クライアントへの影響と移行範囲を確認する
独自DTOからProblem detailsへの移行は、レスポンス契約の変更です。従来のmessageを読む処理は、detailへ変えただけでも修正が必要になります。Content-Typeを厳密に判定するクライアントにも影響します。
移行前に、利用中のクライアントが参照する項目、コードによる分岐、HTTPステータスへの依存を洗い出します。そのうえで、対象エンドポイントと切り替え時期、必要な互換対応を決めます。新しい形式と古い形式が混在する期間は、どのAPIがどちらを返すかを明示します。
新規APIでは、type、必要な拡張メンバー、各項目の型と意味を最初に文書化します。クライアント側も、未知の拡張メンバーが追加されることを考慮し、表示用文言の変更だけで分岐が壊れない構成にします。
まとめ
Spring BootのProblemDetailは、APIエラーの本文をRFC 9457形式へそろえるための型です。typeで問題種別、titleで短い説明、detailで今回の問題を伝え、statusは実際のHTTPステータスと一致させます。
Spring Boot 3.5のspring.mvc.problemdetails.enabled=trueは、Spring MVCの組み込み例外への対応に使います。独自例外はAdviceで公開用の表現へ変換し、codeやerrorsが必要ならAPI固有の拡張として定義します。
形式を整える際は、機械が読む識別値と人が読む説明を分け、内部情報や入力値を無条件に転記しないことが大切です。HTTPステータス・Content-Type・JSONの契約をまとめて確認し、既存クライアントへの影響を整理して移行してください。


コメント