JSON Patch

JSON ドキュメントの部分更新操作を配列で記述する標準フォーマット (RFC 6902)。6 操作の意味と JSON Pointer のエスケープ、test による楽観的ロック、PUT や JSON Merge Patch との使い分けを解説

データ形式API

JSON Patch とは

JSON Patch (RFC 6902) は、JSON ドキュメントに対する部分更新操作を配列形式で記述する標準フォーマットである。HTTP PATCH メソッドと組み合わせて、リソースの一部だけを更新する REST API を実装する際に使われる。Content-Type は application/json-patch+json を指定する。

PUT がリソース全体の置換であるのに対し、PATCH + JSON Patch は「何をどう変更するか」を明示的に記述する。大きなリソースの一部だけを更新する場合、転送データ量が大幅に削減される。

6 つの操作

操作は add / remove / replace / move / copy / test の 6 種類しかない。パッチ本体は操作オブジェクトの配列で、上から順に適用される。

[
  { "op": "add",     "path": "/tags/2",    "value": "urgent" },
  { "op": "remove",  "path": "/tags/0" },
  { "op": "replace", "path": "/status",    "value": "active" },
  { "op": "move",    "from": "/old_name",  "path": "/new_name" },
  { "op": "copy",    "from": "/template",  "path": "/config" },
  { "op": "test",    "path": "/version",   "value": 3 }
]
操作説明用途
add値を追加 (配列の挿入、新フィールドの追加)新しいタグの追加
remove値を削除不要なフィールドの削除
replace値を置換ステータスの更新
move値を移動 (remove + add)フィールド名の変更
copy値をコピーテンプレートからの複製
test値が期待通りか検証 (楽観的ロック)競合検出

path に書くのはプロパティ名ではなく JSON Pointer (RFC 6901) である。/tags/2 はルート直下の tags の 3 番目の要素を指す。ここで最初につまずくのはエスケープで、キー名に含まれる ~~0/~1 へ置き換えなければならない。a/b というキーを更新するパスは /a~1b になる。URL のパスに似ているせいで、キー名をそのまま埋め込んで意図しない階層を指してしまう事故が起きやすい。

操作の細部も仕様が細かく決めている。add は名前の印象と違い、対象が既存のオブジェクトメンバーなら値を置き換える。配列に対する add は指定位置以降の要素を 1 つ後ろへずらす挿入で、位置に要素数より大きい値は書けない。末尾に足したいときは index ではなく - を使う。movefrompath の先祖にできない。自分の子孫へ移動させる指定は仕様違反になる。

test 操作による楽観的ロック

test は 6 操作の中で唯一、文書を書き換えない。更新前に現在の値を検証し、他のクライアントが同時に変更していないことを確かめる前提条件として使う。後述する JSON Merge Patch にはこれに当たる仕組みがないため、検査と更新を 1 リクエストにまとめられる点が JSON Patch を選ぶ主な理由になる。

[
  { "op": "test",    "path": "/version", "value": 3 },
  { "op": "replace", "path": "/status",  "value": "shipped" },
  { "op": "replace", "path": "/version", "value": 4 }
]

version が 3 でなければ後続の replace は実行されず、パッチ全体が適用されなかったものとして扱われる。この原子性は RFC 6902 が明記しており、途中まで適用された状態は残らない。一方で返す HTTP ステータスは RFC 6902 の規定ではない。PATCH メソッドを定めた RFC 5789 が競合状態の例として 409 Conflict を挙げているため、それに合わせる実装が多いという関係である。読んだときのバージョンを条件に書き込むという考え方は、DynamoDB の条件付き書き込みと同じだ。

test の等価性は論理的な比較として定義されている。まず同じ JSON 型であることが前提で、数値は数値として等しいかを見るため 33.0 は等しい。配列は要素数と各位置の値が一致する必要があるが、オブジェクトはメンバーの並び順を問わない。整形やシリアライズの違いで検査が落ちることはない。ただし数値の比較は結局その言語の数値表現に委ねられるので、桁数の極端に多い値を条件にするのは避けたほうがよい。

PUT vs PATCH vs JSON Patch

「注文のステータスを shipped にする」という同じ 1 つの変更を 3 通りで書くと、選択の基準が見えてくる。

// PUT: リソース全体を置換 (全フィールドを送信)
PUT /orders/123
{ "id": "123", "status": "shipped", "items": [...], "address": {...}, "total": 5000 }

// PATCH + JSON Merge Patch: 変更フィールドだけ送信 (シンプル)
PATCH /orders/123
Content-Type: application/merge-patch+json
{ "status": "shipped" }

// PATCH + JSON Patch: 操作を明示的に記述 (高機能)
PATCH /orders/123
Content-Type: application/json-patch+json
[{ "op": "replace", "path": "/status", "value": "shipped" }]
方式長所短所
PUTシンプル、べき等全フィールド送信が必要
JSON Merge Patchシンプル、直感的配列操作が苦手、null で削除
JSON Patch配列操作、test 操作、高精度記述が冗長、操作によってはべき等でない

JSON Merge Patch は RFC 7396 が定めた別のフォーマットで、null を「そのキーを削除する」意味に使う。値として null を保存したいデータに使えないという制約はここから来る。べき等性も方式で違う。PUT と JSON Merge Patch は同じリクエストを 2 回投げても結果が変わらないが、JSON Patch は配列への add のように繰り返すと要素が増える操作を書ける。通信エラーでリトライする前提の API なら、test で前提条件を固定して二重適用を防ぐか、べき等な操作だけで組む。

JSON Patch が適するケース

  • 大きなリソースの一部だけを更新する API
  • 配列の特定要素を操作する必要がある場合
  • 楽観的ロック (test 操作) が必要な場合
  • 変更履歴を操作単位で記録したい場合

逆に、更新するフィールドが数個で配列を触らないなら JSON Patch は過剰だ。クライアント側でパッチを組み立てるコードと、サーバー側で 6 操作すべてを正しく実装する手間が、削減できる転送量に釣り合わない。JSON Merge Patch で足りる場面のほうが実際には多い。

理論と実装の両面から学ぶなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事