JSON Patch入門|add・remove・replace・move・copy・testを例付きで解説
JSON形式のAPIを扱っていると、
「JSON全体ではなく、一部分だけ変更したい」
というケースがあります。
そんなときに利用できる仕組みのひとつが JSON Patch です。
JSON Patchを使うと、
- データを追加する
- データを削除する
- 値を変更する
- データを移動する
- データをコピーする
- 現在の値をチェックする
といった操作をJSONとして表現できます。
この記事では、JSON Patchで利用できる
addremovereplacemovecopytest
の6種類について、実際のJSONを使いながらわかりやすく解説します。
JSON Patchとは?
JSON Patchとは、JSONドキュメントの一部分を変更するためのフォーマットです。
RFC 6902として標準化されており、変更内容そのものをJSONとして表現します。
例えば、次のJSONがあるとします。
{
"name": "Java",
"version": 21
}
version を21から25へ変更したい場合、JSON全体を送るのではなく、次のようなPatchを送れます。
[
{
"op": "replace",
"path": "/version",
"value": 25
}
]
意味としては、
/versionの値を25へ変更する
というものです。
JSON Patchでは、このような操作を配列として複数指定できます。
JSON Patchの基本構造
JSON Patchは基本的に次の形式です。
[
{
"op": "操作",
"path": "対象の場所",
"value": "値"
}
]
主な項目は次のとおりです。
| 項目 | 意味 |
|---|---|
| op | 実行する操作 |
| path | 操作対象となるJSON上の位置 |
| value | 追加・変更する値 |
| from | moveやcopyで使用するコピー元・移動元 |
JSON Patchでは次の6種類の操作が定義されています。
| op | 処理 |
|---|---|
| add | 値を追加する |
| remove | 値を削除する |
| replace | 値を置き換える |
| move | 値を移動する |
| copy | 値をコピーする |
| test | 現在の値をチェックする |
それぞれ詳しく見ていきましょう。
add:値を追加する
add は、新しい値をJSONへ追加するときに使用します。
例えば次のJSONがあるとします。
{
"shelfId": "S001",
"shelfName": "食品棚A"
}
ここへ shelfType を追加します。
[
{
"op": "add",
"path": "/shelfType",
"value": "FOOD"
}
]
適用後は次のようになります。
{
"shelfId": "S001",
"shelfName": "食品棚A",
"shelfType": "FOOD"
}
配列へaddする
配列に対しても追加できます。
例えば、
{
"products": [
{
"productId": "P001",
"productName": "カップラーメン"
}
]
}
というJSONがあるとします。
次のPatchを適用します。
[
{
"op": "add",
"path": "/products/1",
"value": {
"productId": "P002",
"productName": "レトルトカレー"
}
}
]
結果は、
{
"products": [
{
"productId": "P001",
"productName": "カップラーメン"
},
{
"productId": "P002",
"productName": "レトルトカレー"
}
]
}
となります。
配列の最後に追加する
配列の末尾へ追加したい場合は - を利用できます。
[
{
"op": "add",
"path": "/products/-",
"value": {
"productId": "P003",
"productName": "パスタ"
}
}
]
/products/-
とすることで、products配列の末尾へ追加できます。
addで注意したいポイント
少し意外ですが、オブジェクトに対する add は対象キーがすでに存在する場合、その値を置き換えます。
例えば、
{
"shelfName": "食品棚A"
}
に、
[
{
"op": "add",
"path": "/shelfName",
"value": "食品棚B"
}
]
を適用すると、
{
"shelfName": "食品棚B"
}
になります。
そのため、
add = 必ず新規追加
というわけではない点に注意しましょう。
remove:値を削除する
remove は、指定した値をJSONから削除します。
元のJSONを次のようにします。
{
"shelfId": "S001",
"shelfName": "食品棚A",
"shelfType": "FOOD"
}
shelfType を削除する場合は、
[
{
"op": "remove",
"path": "/shelfType"
}
]
とします。
結果は、
{
"shelfId": "S001",
"shelfName": "食品棚A"
}
となります。
removeでは value は必要ありません。
削除対象を path だけで指定します。
配列から削除する
次の配列があるとします。
{
"products": [
{
"productId": "P001"
},
{
"productId": "P002"
},
{
"productId": "P003"
}
]
}
2番目の要素を削除します。
[
{
"op": "remove",
"path": "/products/1"
}
]
結果は、
{
"products": [
{
"productId": "P001"
},
{
"productId": "P003"
}
]
}
となります。
ここで重要なのが、配列をremoveするとインデックスが詰まることです。
P003はもともと、
/products/2
でしたが、P002削除後は、
/products/1
になります。
複数のJSON Patchを順番に適用する場合は、このインデックス変化に注意が必要です。
replace:値を置き換える
replace は、既存の値を別の値へ変更します。
例えば、
{
"shelfId": "S001",
"shelfName": "食品棚A"
}
というJSONに対して、
[
{
"op": "replace",
"path": "/shelfName",
"value": "食品棚B"
}
]
を適用します。
結果は、
{
"shelfId": "S001",
"shelfName": "食品棚B"
}
になります。
replaceはイメージとして、
remove
↓
add
を同じ場所に対して実行する操作と考えるとわかりやすいでしょう。
addとの違い
addとreplaceは似ていますが、大きな違いがあります。
replace の場合、対象となる path が存在している必要があります。
例えば、
{
"shelfId": "S001"
}
に対して、
[
{
"op": "replace",
"path": "/shelfName",
"value": "食品棚A"
}
]
とすると、shelfName が存在しないため正常にreplaceできません。
一方、addなら新しい項目として追加できます。
move:値を移動する
move は、ある場所に存在する値を別の場所へ移動します。
moveでは、
from
と
path
の2つを指定します。
例えば、
{
"shelf": {
"name": "食品棚A"
},
"archive": {}
}
というJSONがあるとします。
次のPatchを適用します。
[
{
"op": "move",
"from": "/shelf/name",
"path": "/archive/name"
}
]
結果は、
{
"shelf": {},
"archive": {
"name": "食品棚A"
}
}
となります。
ポイントは、元の値が削除されることです。
moveはイメージとして、
fromからremove
↓
pathへadd
という処理になります。
copy:値をコピーする
copy はmoveと似ていますが、元データを削除しません。
例えば、
{
"shelf": {
"name": "食品棚A"
},
"backup": {}
}
というJSONに、
[
{
"op": "copy",
"from": "/shelf/name",
"path": "/backup/name"
}
]
を適用します。
結果は、
{
"shelf": {
"name": "食品棚A"
},
"backup": {
"name": "食品棚A"
}
}
となります。
moveとの違いを整理すると、
| 操作 | コピー元 |
|---|---|
| move | 消える |
| copy | 残る |
となります。
test:現在の値をチェックする
test はJSONを書き換える操作ではありません。
指定された場所が、期待している値になっているか確認するために使用します。
例えば、
{
"status": "PUBLISHED"
}
というJSONがあるとします。
次のPatchを実行します。
[
{
"op": "test",
"path": "/status",
"value": "PUBLISHED"
}
]
現在のstatusが PUBLISHED なのでtestは成功します。
一方、
[
{
"op": "test",
"path": "/status",
"value": "DRAFT"
}
]
の場合、現在値と一致しないため失敗します。
testは更新前チェックにも使える
例えば、
[
{
"op": "test",
"path": "/status",
"value": "DRAFT"
},
{
"op": "replace",
"path": "/status",
"value": "PUBLISHED"
}
]
というPatchを考えてみます。
意味としては、
現在statusがDRAFTか確認
↓
DRAFTならPUBLISHEDへ変更
となります。
想定していない状態のデータを書き換えることを防ぐ用途でも利用できます。
pathはJSON Pointerで指定する
JSON Patchで非常に重要なのが path です。
pathはJSON Pointerと呼ばれる形式で指定します。
例えば、
{
"shelves": [
{
"shelfId": "S001",
"products": [
{
"productId": "P001",
"productName": "カップラーメン"
}
]
}
]
}
というJSONがあった場合、
/shelves/0/products/0/productName
は、
shelves
↓
0番目
↓
products
↓
0番目
↓
productName
を意味します。
そのため、
[
{
"op": "replace",
"path": "/shelves/0/products/0/productName",
"value": "しょうゆラーメン"
}
]
とすると、
{
"shelves": [
{
"shelfId": "S001",
"products": [
{
"productId": "P001",
"productName": "しょうゆラーメン"
}
]
}
]
}
となります。
複数のJSON Patchは上から順番に実行される
JSON Patchでは複数の操作をまとめて指定できます。
例えば、
[
{
"op": "add",
"path": "/shelfType",
"value": "FOOD"
},
{
"op": "replace",
"path": "/shelfName",
"value": "食品棚B"
},
{
"op": "remove",
"path": "/description"
}
]
のように指定できます。
重要なのは、それぞれが独立して元JSONへ適用されるわけではないことです。
処理イメージは、
元JSON
↓
1個目のPatchを適用
↓
変更後JSON
↓
2個目のPatchを適用
↓
変更後JSON
↓
3個目のPatchを適用
となります。
つまり、前のPatchによって変更されたJSONに対して、次のPatchが実行されます。
そのため、配列へのaddやremoveが含まれる場合は特に注意が必要です。
配列操作ではインデックスに注意する
JSON Patchを実務で利用するとき、特に問題になりやすいのが配列です。
例えば、
{
"products": [
{
"productId": "P001"
},
{
"productId": "P002"
},
{
"productId": "P003"
}
]
}
があるとします。
最初に、
{
"op": "remove",
"path": "/products/0"
}
を実行すると、
{
"products": [
{
"productId": "P002"
},
{
"productId": "P003"
}
]
}
となります。
この時点でP002は、
/products/1
ではなく、
/products/0
になります。
そのため、後続Patchが変更前のインデックスを前提としていると、意図していないデータを操作してしまう可能性があります。
JSON Patchを時系列で蓄積するようなシステムでは、特に注意したいポイントです。
JSON Patchでよく使う操作は?
6種類ありますが、一般的なCRUDに置き換えると理解しやすくなります。
| JSON Patch | イメージ |
|---|---|
| add | CREATE / 追加 |
| remove | DELETE / 削除 |
| replace | UPDATE / 更新 |
| move | 移動 |
| copy | 複製 |
| test | 更新前チェック |
通常のAPI更新で特によく利用するのは、
addremovereplace
の3つでしょう。
データ構造やシステムによっては、addとremoveだけを利用して変更履歴を表現する設計も考えられます。
HTTP PATCHで送信する場合
JSON PatchはHTTPのPATCHメソッドと組み合わせて利用できます。
例えば、
PATCH /api/shelves/S001
Content-Type: application/json-patch+json
リクエストBodyとして、
[
{
"op": "replace",
"path": "/shelfName",
"value": "新しい食品棚"
}
]
を送信します。
一般的なJSONで使われる、
application/json
ではなく、JSON Patchでは、
application/json-patch+json
というMedia Typeが定義されています。
JSON Patchの6操作まとめ
最後に、それぞれの操作を整理します。
| 操作 | 内容 | 主な項目 |
|---|---|---|
| add | 値を追加する | path、value |
| remove | 値を削除する | path |
| replace | 既存値を変更する | path、value |
| move | 値を移動する | from、path |
| copy | 値をコピーする | from、path |
| test | 値が一致するか確認する | path、value |
特に覚えておきたいポイントは次のとおりです。
- JSON Patchには6種類の操作がある
- pathはJSON Pointer形式で指定する
- Patchは配列の先頭から順番に適用される
- addは既存キーに対して実行すると値を置き換える
- removeで配列要素を消すと後続要素のインデックスが変わる
- replaceは対象データが存在している必要がある
- moveでは移動元のデータがなくなる
- copyではコピー元のデータは残る
- testを利用すると更新前の状態確認ができる
JSON Patch自体の構造はシンプルですが、複数のPatchを時系列で管理したり、配列に対する変更を積み重ねたりすると、設計上考えるべきポイントが増えてきます。
特に「過去のPatchを変更・削除した場合に後続Patchをどう扱うか」は、JSON Patchを履歴として保存するシステムでは重要な設計ポイントになります。
JSON Patchを利用する際は、単純なAPI更新だけでなく、変更履歴をどのように管理するのかまで含めて設計することが重要です。
参考仕様
- RFC 6902:JSON Patch
- RFC 6901:JSON Pointer
是非フォローしてください
最新の情報をお伝えします
