🥳

REST APIの理想のエラーレスポンスを再考する

に公開

API を設計する際、エラーレスポンスの定義は意外と揺れやすく、これまでの経験では「適当に作られている」ケースも少なくありません。
過去の実装の適当さに煩わされて泣くことが多く、「なんとか助かりたい」という思いからこの記事を書きました。

理想のエラーレスポンスとは

  • 構造が揃っていることで、コード値や理由を決まった場所に格納可能
  • フレームワークや言語特性に依存せず、実装の一貫性を保つ
  • 静的型付け言語でも後続処理が安定
  • 複数エラーを統一的に処理可能
  • バリデーションエラー、排他処理エラー、認証エラーなどを同じ形式で扱える

現状の課題

  • 形式の不統一
    • フィールドの意味や構造が定義されていないため、実装によって異なる値が入る
    • 複数エラーを返しにくく、一度に一つのエラーしか返さない場合が多い
    • 判別用のキー(HTTP ステータスコード、独自コード、error キーなど)が状況によってばらつく
  • フレームワーク依存や模倣による問題
    • バリデーションエラーの構造やキー名がフレームワークごとに異なるため、後続処理が安定しない
    • 処理全体のエラーとフィールド別バリデーションエラーが混在し、形式が統一されていない
    • Web フレームワークではフィールド名をキーにした map 形式で返すことが多く、静的型付け言語では扱いにくい
    • 複数エラーにした場合で、完全に列挙型にした場合、レスポンスから一発でエラーか判別しにくい(JSON:API のエラー定義では代表となるエラーコードが定義されておらず、同列のエラーが列挙されている)
  • その他の設計上の課題
    • 複数のエラーが発生しても、処理が途中で停止し、一度に全エラーを返せない・(返さない実装に誘導してしまう)
      • (エラーレスポンスを複数含める実装を想定していないから、一個ずつエラーを返すロジックを実装してしまうなど)
    • エラーコードの分類や HTTP ステータスコードの選定基準が明確でない
    • リトライ可能な状況や方針がわかりにくい

同様の内容を扱う記事について

さまざまなサービスのエラー設計を比較した記事も参考になります。

現状の最も参考になる定義資料

これらの前提から最終的に参考にすべき資料としては、こちらの3つが最新で信頼度が高そうでした。

馴染みのない単語や、そこまで準備しきれないと感じる点があったため、これらをベースに扱いやすく、馴染みのある単語を使って構造を考えてみました。

理想のエラーレスポンス構造

  • status : HTTP ステータスコード
  • code : 独自のエラーコード
  • title : 短く分かりやすいタイトル
  • message : 詳細説明
  • errors : 複数エラーの配列
    • code : 個別エラーコード
    • title : 短いタイトル
    • message: 詳細説明
    • source: 原因(pointer, parameter, header
    • meta: ID やバリデーション追加情報など

OpenAPI 3.1形式のスキーマ

components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        status:
          type: integer
          format: int32
          description: HTTP status code as integer (e.g., 422)
        code:
          type: string
          description: Machine-readable error code
        title:
          type: string
          description: Short summary of the error
        message:
          type: string
          description: Human-readable error message
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ErrorItem'
      required:
        - status
        - code
        - title
        - message
        - errors

    ErrorItem:
      type: object
      properties:
        code:
          type: string
          description: Machine-readable error code
        title:
          type: string
          description: Short summary of the error
        message:
          type: string
          description: Human-readable error message
        source:
          $ref: '#/components/schemas/ErrorSource'
        meta:
          type: object
          additionalProperties: true
      required:
        - code
        - source

    ErrorSource:
      type: object
      properties:
        pointer:
          type: string
          description: JSON Pointer to the field causing the error
        header:
          type: string
          description: Header field name causing the error
        parameter:
          type: string
          description: Form parameter field name causing the error
      additionalProperties: false

具体例とポイント

実際にこれらがうまく当てはまるかみていきましょう。

認証エラー(単一エラー)の例

OAuthにおける認証エラー時のレスポンスでは、HTTPstatus code401とともに、error = エラーコード文字列 error_description = エラーの説明内容を返すのが標準(RFC 6750)です、このようなケースにも当てはめられます。

OAuth認証エラーの例

HTTP/1.1 401 Unauthorized 
WWW-Authenticate: Bearer realm="example",
                   error="invalid_token",
                   error_description="The access token expired"

jsonにあてはめると

{
  "status": 401,
  "code": "invalid_token",
  "title": "Unauthorized",
  "message": "The access token expired",
  "errors": [
    {
      "code": "invalid_token",
      "source": { "header": "Authentication" }
    }
  ]
}

HTTP のステータスコードやエラーコード、説明をすんなりマッピングして格納できました。
また、errors の source に header を入れることで、どこでエラーが発生したのかを明示できる点にもメリットがあります。

単一のコンフリクトエラー

単一エラーの場合は、エラー詳細が必要なければ省略もできます。

{
  "status": 409,
  "code": "resource_locked",
  "title": "Conflict Error",
  "message": "排他処理のため、操作を完了できませんでした。"
}

バリデーションエラー

複数エラーも想定するケースにあわせて問題なく入れられそうです。

{
  "status": 400,
  "code": "validation_error",
  "title": "Validation Error",
  "message": "リクエストデータに複数のバリデーションエラーがあります。",
  "errors": [
    {
      "title": "必須",
      "message": "企業IDは必ず入力してください。",
      "code": "required_field_missing",
      "source": { "pointer": "/employees/0/company_id" },
      "meta": { "field": "company_id" }
    },
    {
      "title": "日付形式エラー",
      "message": "締め処理の日付の形式は YYYY-MM-DD の形式で入力してください。",
      "code": "invalid_format",
      "source": { "pointer": "/employees/0/closing_date" },
      "meta": { "expected_format": "YYYY-MM-DD", "provided_value": "12/31/2025" }
    },
    {
      "title": "最大値超過",
      "message": "賞与額は5,000,000以下で入力してください",
      "code": "value_exceeds_max",
      "source": { "pointer": "/employees/0/bonus_amount" },
      "meta": { "max_value": 5000000, "provided_value": 8000000 }
    }
  ]
}

ポイント

複数エラーが一番要素が多いのでポイントをまとめてみました。

  1. 上位レベルのステータスで判別可能

    "status": 400,
    "code": "validation_error"
    
    • status や code を見れば「バリデーションエラーである」と処理側で簡単に判定可能。
    • UI 側や後続処理で「詳細エラーを表示する・しない」を分けるロジックが作りやすい。
  2. ユーザー向けとシステム向けの情報を分けている

    • title / message は人間が理解できる文言。
    • code / source はプログラムが原因を特定するための情報。
    • "title": "必須",
      "code": "required_field_missing",
      "source": { "pointer": "/employees/0/company_id" }
      
  3. 複数エラーをまとめて返せる

    • errors 配列に複数のエラーオブジェクトを入れることで、一度のリクエストで全エラーを返すことができる。
    • ユーザー体験の改善と、フロント側での処理効率化につながる。
  4. meta で補足情報を追加可能

    • フィールドごとの追加情報や元データなどを格納できる。
    • "meta": { "expected_format": "YYYY-MM-DD", "provided_value": "12/31/2025" }
      
    • これにより、UI で正しい入力例を表示したり、デバッグ用にログに残すことが簡単になる。
  5. source.pointer の役割

    • エラーがどのデータに関連しているかを明示。
    • 配列やネストしたオブジェクトでも正確に場所を指せるので、フォームにエラー表示する際に便利。

このように構造化することで、フロントもバックも効率的に扱える 拡張性の高いエラーフォーマット になるのではないでしょうか?

残課題

  • JSON:API の links や id の取り扱い
    • JSON:API では links の利用も定義されていますが、今回は採用していません。本来 links はエラーが発生したリソースや関連エンドポイントを示すために使う要素です。ただ、エラーオブジェクト全体との整合性を担保しながら links を適切に管理するためには、運用上の整理やルール決めが別途必要になります。 現時点では、運用負荷とメリット・デメリットを踏まえてlinksの運用方針を明確にできていないため、この記事の設計案ではあえてlinksを省略 しました。
    • 同様に JSON:API で定義されている id についても、今回の設計からは除外しています。本来、id はエラー個別の識別子として利用でき、trace_id のようなリクエスト単位のトラッキング情報を格納する運用も可能です。ただ、これを導入する場合は エラーレスポンスに限らず、通常レスポンスを含めた全 API の共通仕様として扱う必要がある ため、部分的な採用は避けたいと判断しました。現段階では API 全体の統一的な識別子運用ルールを定められていないため、本稿では id の利用も見送っています。将来的にトレーサビリティ強化を図るタイミングで再検討するのがよいと考えています。
  • エラー種別とコードの典型パターン整理
    • エラー種別とコード体系については、典型的なパターンを整理し、どの種類のエラーにどのコードを割り当てるのかを一貫して参照できるようにする必要があります。
    • 現時点ではまだ体系化しきれていないため、本稿では詳細を扱っていませんが、API 全体の整合性を保つためにも、別途きちんと整理してまとめる必要があります。
  • リトライ可能かどうかの明示的定義
    • リトライ可能かどうかの明示的な定義についても、今後整理が必要な領域です。 特に、排他制御や一時的なサーバ負荷など「再試行すれば成功する可能性があるエラー」と、バリデーションエラーのように「何度リトライしても成功しないエラー」を明確に区別しておくことで、クライアント側の実装が大きく簡略化されます。
    • このテーマだけで 1 本の記事になるほど論点が多いため、本稿では詳細を扱わず、別記事としてまとめたいと思います。

まとめ

この形式なら、単一エラーでも複数エラーでも同じ型で扱えmeta も柔軟に拡張できます。
静的型付け言語での実装や、フロントエンドでのエラー表示ロジックもシンプルになり、API 全体の整合性も確保しやすくなるはずです。

統一フォーマットを採用することで、フレームワーク依存の癖 や プロジェクトごとの書き方の揺れ に悩まされることも減り、エラー処理の保守性が大幅に改善が期待できます。
長期運用する業務システムでは、こうした基盤レベルの整理が後々の開発速度や品質に直接効いてくると感じています。

以上です。

本記事が、同じようにエラーレスポンスに悩んでいる方の助けになれば幸いです。
ご意見や別のアプローチ例などがあれば、ぜひ共有してください。

jinjerテックブログ

Discussion