JSON Toolkit

JSON API データ契約設計の要点

要点:出荷後に変えるのが非常に難しい六つ。レスポンスエンベロープ、エラー形式、ページネーションの形、ID と時刻の表現、null と欠落の意味論、バージョン進化です。

中級約 13 分で読めます2026-10-30 公開2026-10-30 更新
JSON API データ契約設計の要点のカバー画像

要点:ページネーションや命名の細部を論じる前に、レスポンスエンベロープ、エラー形式、ID と時刻の表現、null と欠落の意味論を契約として固定しましょう。トップレベルは常に data エンベロープで包み、後からページネーションやトレース ID を既存の呼び出し元を壊さずに追加できるようにします。

API 設計に標準の答えはありませんが、「一度選ぶと取り戻しにくい」決定がいくつかあり、最初の一行を書く前に考えておくのが賢明です。

1. レスポンスを包むか包まないか

三つのスタイル:

// スタイル A:裸のレスポンス
{ "id": 1, "name": "山田" }

// スタイル B:data エンベロープ
{ "data": { "id": 1, "name": "山田" } }

// スタイル C:data + メタデータ
{ "data": { "id": 1, "name": "山田" }, "meta": { "requestId": "a1b2" } }
スタイル 利点 欠点
裸のレスポンス 最も単純。フロントエンドの階層が一つ少ない メタデータを付けられない。トップレベル配列は JSON ハイジャックと進化のリスクを持つ
data エンベロープ meta / errors / links を足しやすい ネストが一段増え、フロントエンドは res.data.data と書く
data + メタデータ 可観測性が最良 meta に何を入れるかの厳格な合意が必要

推奨:リストも詳細も一律に data エンベロープを使い、トップレベルを配列にしないこと。そうすれば後からページネーションやトレース ID を足しても契約は壊れません。

2. エラー形式:RFC 9457 をコピーする

独自のエラー構造を発明するのは、協業コストの巨大な源です。RFC 9457(RFC 7807 を置き換える)は application/problem+json を定義しています。

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "残高不足",
  "status": 422,
  "detail": "口座残高は 12.00 元、今回の引き落としには 30.00 元が必要です",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
フィールド 意味 必須
type 安定した問題種別 URI 推奨
title 人間向けの短い要約 推奨
status HTTP ステータスコード 推奨
detail 今回何が起きたかの具体的な説明 任意
instance 問題が起きたリソースインスタンス 任意
拡張フィールド 任意の業務フィールド 任意

拡張フィールド(上の balance / required など)は仕様で許されており、フロントエンドは detail のテキストをパースする代わりに、それらを使って精密なメッセージを組み立てられます。

3. ページネーション:カーソルがオフセットに勝る

// オフセットページネーション
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// カーソルページネーション
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
観点 オフセットページネーション カーソルページネーション
実装の難しさ 低い(LIMIT/OFFSET) 中程度(安定した並び替えキーが必要)
深いページの性能 悪い(オフセットが大きいほど遅い) 良い
データ変化時 重複や欠落 安定
ページジャンプ 可能 不可能
総件数が必要 追加のカウントが必要 不要

推奨:管理画面の表(ページジャンプと総件数が必要)にはオフセット、ユーザー向けの無限フィードにはカーソルを使います。

4. ID と時刻

4.1 ID は常に文字列

理由は型の記事で扱いました。JavaScript 側では 2^53 を超える値が静かに精度を失います。 現在の ID が小さな自動増分整数でも、文字列にしておけば後でスノーフレーク ID に切り替える余地が残ります。

{ "id": "1000000000000000001" }

4.2 時刻には RFC 3339 文字列

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • ミリ秒タイムスタンプの数値は使わない。読みにくく、単位(秒/ミリ秒)を頻繁に間違えます。
  • タイムゾーンを明示する。Z かオフセットのどちらかで。タイムゾーンのない「ローカル時刻」は決して使わない。
  • フィールドは一律 xxxAt と命名し、一目で時刻点だと分かるようにする。

5. null と欠落

どちらかを選び、ドキュメントに書き込まなければなりません。

方法 「値なし」の表現 「変更しない」の表現
方法 A フィールドの値が null リクエストでフィールドを省略
方法 B フィールドが存在しない リクエストでフィールドを省略

方法 A の問題は、PATCH で「省略」と「null として送信」が異なる意味になり、誤解しやすいことです。 方法 B の問題は、フロントエンドの JSON.stringify が値 undefined のフィールドを落とし、たまたま意味論と一致することです。

実践的な推奨:

  • レスポンスボディには方法 A(null は明確に値なしを意味し、フロントエンドは undefined を区別する必要がない)。
  • リクエストボディ(特に PATCH)には方法 B を使い、Merge Patch か JSON Patch と組み合わせて意味論を明示する。

6. フィールド命名と構造

決定 よくある選択 注意点
命名スタイル camelCase(JS エコシステム)/ snake_case(Python、データベース) プロジェクト全体で統一し、混ぜない
enum 値 小文字とアンダースコア、"pending_review" 数値は使わない。読みにくい
真偽値フィールド isActive / enabled status: 0/1 は使わない
配列フィールド 複数形の名詞、items 空配列は [] を返し、決して null にしない
ネストの深さ 3 階層以内 それより深いなら分割するか expand パラメータを提供

空配列に null ではなく [] を返すことは非常に重要です。フロントエンドの data.items.map(...) が null でクラッシュしません。

7. 進化の戦略

変更 互換性を壊すか 扱い方
任意フィールドを追加 いいえ そのまま出荷
必須フィールドを追加 はい 既定値を足すか、バージョンでリリース
フィールドを削除 はい まず非推奨とマークし、呼び出し元を観察してから削除
フィールドの型を変更 はい 新しいフィールド名を足し、移行期間中は二重書き込み
enum の値集合を変更 場合による クライアントは未知値のフォールバックを持たなければならない
フィールドの意味を変更 はい 新しいフィールドとして扱う

規律の三つの規則:

  1. クライアントは未知のフィールドに寛容でなければならない(additionalProperties: false で自分を縛らない)。
  2. クライアントは未知の enum 値にフォールバック分岐を持たなければならない。
  3. 非推奨フィールドは少なくとも一つのメジャーバージョンは残し、レスポンスヘッダーかドキュメントで Deprecation をマークする。

8. 完全な例

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 徹底ガイド",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

このレスポンスは上のすべての慣習を反映しています。data エンベロープ、文字列 ID、文字列の金額、タイムゾーン付きタイムスタンプ、 欠落を意味する null、空配列、カーソルページネーション、可観測性情報の meta。

9. チェックリスト

  • [ ] トップレベルは配列でなく、一律に data エンベロープを使う。
  • [ ] エラー応答は RFC 9457 に従い、安定した type を含む。
  • [ ] ページネーションの形が業務シナリオに合う。
  • [ ] ID は文字列で、時刻は RFC 3339。
  • [ ] null と欠落の意味論がドキュメントに書かれている。
  • [ ] 命名スタイルがプロジェクト全体で統一され、enum は小文字文字列。
  • [ ] クライアントが未知フィールドと未知 enum 値のフォールバックを持つ。
  • [ ] 破壊的変更はバージョニングか二重書き込みの移行を通す。

FAQ

進化可能な JSON API はどう設計すればよいですか?

リストも詳細も一律に data エンベロープで包み、トップレベルを配列にしないことで、契約を壊さずにページネーションとトレース ID を足せます。ID は文字列、時刻は RFC 3339 を使い、null と欠落の意味論をドキュメントに書きます。新しいフィールドは任意で意味的に独立に保ち、クライアントに未知フィールドと enum 値のフォールバックを持たせます。

独自のエラー形式を発明しないのはなぜですか?

独自のエラー構造を発明するのは協業コストの巨大な源です。RFC 9457 の application/problem+json をコピーし、問題を安定した type で識別し、balance や required のような拡張フィールドを許せば、フロントエンドは detail のテキストをパースせずに精密なメッセージを組み立てられます。

すべてのエンドポイントがカーソルページネーションを使うべきですか?

いいえ。管理画面の表はページジャンプと総件数が必要なのでオフセットの方が合います。ユーザー向けの無限フィードはカーソルを使うべきで、深いページでも安定し、データ変化時の重複や欠落を避けられます。シナリオがページジャンプと総件数を必要とするかで選んでください。