APIで一部のfieldだけを更新するとき、単純なJSON objectでは「値を設定する」「memberを削除する」「現在値が一致するときだけ変更する」といった操作を明確に表せないことがあります。
JSON Patchは、JSON documentへの変更をoperationのarrayで表す形式です。更新内容だけでなく、操作順、対象path、事前条件をdataとして受け渡せます。一方、array indexのずれや再送による二重追加、権限外fieldの変更には注意が必要です。
この記事では、JSON Pointer、6種類のoperation、test、原子性、冪等性、Python実装を、設計データ更新の例で整理します。
JSON Patchで解決すること
RFC 6902のJSON Patch documentは、operation objectを並べたJSON arrayです。HTTP PATCHで送る場合のmedia typeはapplication/json-patch+jsonです。
更新前のassetを次のobjectとします。
{
"id": "asset-42",
"version": 7,
"name": "Hydraulic unit A",
"tags": ["hydraulic"],
"note": null
}
名称変更、tag追加、現在versionの確認を一つのpatchで表せます。
[
{"op": "test", "path": "/version", "value": 7},
{"op": "replace", "path": "/name", "value": "Hydraulic unit B"},
{"op": "add", "path": "/tags/-", "value": "inspected"}
]
各array要素が1 operationです。opが操作、pathが対象位置、valueが比較または設定する値です。上から順に適用され、途中結果が次のoperationの入力になります。
JSON Merge Patchとの違い
JSON Merge Patchは更新後に近いobjectを送り、JSON Patchは変更手順をarrayで送ります。
| 観点 | JSON Patch | JSON Merge Patch |
|---|---|---|
| body | operationのarray | 更新後に近いobject |
| member削除 | remove | null |
| JSON nullの設定 | valueにnullを指定可能 | nullが削除を表す |
| 配列の部分操作 | indexや末尾追加 | 原則array全体を置換 |
| 事前条件 | testを使用可能 | 形式内にはない |
JSON Patchなら、noteをJSON nullへ設定する操作と、note memberを削除する操作を分けられます。
[{"op": "replace", "path": "/note", "value": null}]
[{"op": "remove", "path": "/note"}]
DBのNULLと、objectにmemberが存在しない状態も別に扱います。
6種類のoperationを理解する
RFC 6902は次のoperationを定義しています。
| op | 必要なmember | 動作 | 注意点 |
|---|---|---|---|
| add | path、value | 値を追加 | 既存object memberなら置換 |
| remove | path | 対象を削除 | 対象が必要 |
| replace | path、value | 既存値を置換 | 対象が必要 |
| move | from、path | 値を移動 | fromを削除する |
| copy | from、path | 値を複製 | from側にも認可が必要 |
| test | path、value | 現在値を比較 | 不一致なら失敗 |
removeを同じdocumentへ2回適用すると、2回目は対象がないため失敗します。独自のincrementやappend_uniqueをRFC 6902のoperationとして混在させず、必要なら専用endpointまたは独自media typeとして定義します。
pathはJSON Pointerで表す
pathとfromにはRFC 6901のJSON Pointerを使います。rootからmemberをたどり、各tokenの前へ/を付けます。
/name objectのname
/tags/0 tags arrayの先頭要素
/tags/- tags arrayの末尾へ追加するときの位置
/dimensions/x dimensions object内のx
key内の/は~1、~は~0へescapeします。受信側では文字列のprefixだけでなく、解決後のfieldにも認可を適用します。
testで古いデータによる上書きを防ぐ
testは対象値と指定値を比較し、不一致なら後続operationを適用しません。
[
{"op": "test", "path": "/version", "value": 7},
{"op": "replace", "path": "/material", "value": "S45C"}
]
現在versionが8なら、このpatchは失敗します。clientは最新版を再取得し、自分の変更と他の変更を比較します。失敗後にtestだけを8へ書き換えて自動再送すると、競合検出を無効にしてしまいます。
強いETagとIf-Matchを併用する場合、ETagはrepresentation全体、testはdocument内部の値に対する事前条件として役割を決めます。
全operationを原子的に扱う
RFC 5789は、serverがPATCHの変更集合を原子的に適用するよう定めています。5個中4個だけ成功させる処理ではありません。
Content-Typeとbody sizeを確認
↓
JSONと各operationを検証
↓
現在documentのcopyへ全操作を適用
↓
結果をschema・業務規則で検証
↓
DB transactionでversion条件付き更新
memory上のobjectを途中まで直接変更せず、全検証が成功した結果だけを保存します。
Pythonでallowlist付きpatchを適用する
次の例はpython-json-patchを使い、許可するoperationとpathの組み合わせを限定します。idやversionの書き換えは禁止し、先頭のtest /versionだけを許可します。
from copy import deepcopy
import jsonpatch
ALLOWED = {
("replace", "/name"),
("add", "/tags/-"),
}
def apply_asset_patch(current: dict, operations: list[dict]) -> dict:
if not isinstance(operations, list) or not 1 <= len(operations) <= 20:
raise ValueError("patchは1〜20件のarrayが必要です")
expected = {"op": "test", "path": "/version", "value": current["version"]}
if operations[0] != expected:
raise ValueError("先頭に現在versionのtestが必要です")
for operation in operations[1:]:
if not isinstance(operation, dict):
raise TypeError("operationはobjectである必要があります")
if (operation.get("op"), operation.get("path")) not in ALLOWED:
raise ValueError("許可されていない更新です")
try:
candidate = jsonpatch.apply_patch(
deepcopy(current), operations, in_place=False
)
except jsonpatch.JsonPatchException as exc:
raise ValueError("patchを適用できません") from exc
if not isinstance(candidate.get("name"), str) or not candidate["name"].strip():
raise ValueError("nameは空でない文字列が必要です")
if not isinstance(candidate.get("tags"), list):
raise ValueError("tagsはarrayが必要です")
candidate["version"] = current["version"] + 1
return candidate
current = {
"id": "asset-42",
"version": 7,
"name": "Hydraulic unit A",
"tags": ["hydraulic"],
}
patch = [
{"op": "test", "path": "/version", "value": 7},
{"op": "replace", "path": "/name", "value": "Hydraulic unit B"},
{"op": "add", "path": "/tags/-", "value": "inspected"},
]
updated = apply_asset_patch(current, patch)
assert current["version"] == 7
assert updated["version"] == 8
assert updated["tags"] == ["hydraulic", "inspected"]
libraryがpatchを適用できたことと、結果が業務schemaへ適合することは別です。実務では全fieldを再検証し、DBのversion条件付きUPDATEにつなげます。
配列indexと再送に注意する
array indexは先行operationで変化します。/tags/0を削除すると、元のindex 1はindex 0へ移動します。操作対象が集合なら、各要素へ安定したIDを持たせて別resourceにする方が安全です。
同じpatchを再実行したときの結果もoperationで異なります。
| operation例 | 再実行時 |
|---|---|
| 同じ値へのreplace | 結果は同じになりやすい |
| object memberへのadd | 同じkeyなら置換 |
/tags/-へのadd | 要素が再び増える |
| remove | 2回目は対象不存在で失敗 |
| move | 元位置が消えて失敗し得る |
versionのtestは二重適用を防げますが、初回成功後にresponseだけ失われた再送も失敗します。初回結果を再返却したい場合は、request IDまたはidempotency keyと処理結果をserver側へ保存します。
異常系とセキュリティを設計する
最低限、test不一致、未知のop、権限外path、存在しないreplace対象、~0・~1を含むkey、null設定とmember削除、同一requestの再送、DB競合をテストします。
JSON Patchは任意pathを書き換えられるため、受信bodyをそのままdomain objectへ適用するとmass assignmentにつながります。
- operationとpathの組み合わせをallowlist化する
- moveとcopyの
fromにも認可を適用する role、owner_id、approvedなどはfield単位で保護する- operation数、nesting、array size、文字列長を制限する
- tokenや内部メモをpatch対象とresponseから分離する
- patchをlogへ残す場合は機密値を除去する
pathを知っていることは更新権限の証明ではありません。resource単位の認可後にfield単位の認可も行います。
DB・CAD・設計データへ再利用する
JSON Patchをそのまま動的SQLへ変換せず、許可されたpathをdomain fieldと更新列へmappingします。SQLではparameter bindingを使います。
CAD属性やBOMでarray indexが不安定なら、部品ID、属性ID、revisionを持つresourceへ分けます。巨大なCAD fileはpatchへ含めず、file ID、content hash、metadataを変更し、binary本体は別storageでversion管理します。
監査にはresource ID、更新前後のversion、patch、request ID、実行者を記録します。秘密情報を除いたpatch履歴は、変更追跡や再現testへ利用できます。
まとめ
JSON Patchは、部分更新を操作列として表すdata形式です。値だけでなく順序、path、事前条件を契約にできます。
- bodyをoperation objectのarrayとして検証する
- pathにはJSON Pointerを使う
- add・remove・replaceの存在条件を区別する
testまたはIf-Matchで古いdataの上書きを防ぐ- 全operationをcopy上で検証し、DBへ原子的に保存する
- array indexと再送時の非冪等性に注意する
- opとpathをallowlist化し、適用後のdocumentも再検証する
- request IDで通信再送による二重処理を防ぐ
この契約をAPI、DB、Python、CAD metadataへ共通化すれば、競合や再送があっても意図しない上書きや二重追加を防げます。

