JSON Patch入門|add・remove・replace・move・copy・testを例付きで解説

JSON形式のAPIを扱っていると、

「JSON全体ではなく、一部分だけ変更したい」

というケースがあります。

そんなときに利用できる仕組みのひとつが JSON Patch です。

JSON Patchを使うと、

  • データを追加する
  • データを削除する
  • 値を変更する
  • データを移動する
  • データをコピーする
  • 現在の値をチェックする

といった操作をJSONとして表現できます。

この記事では、JSON Patchで利用できる

  • add
  • remove
  • replace
  • move
  • copy
  • test

の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追加・変更する値
frommoveや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イメージ
addCREATE / 追加
removeDELETE / 削除
replaceUPDATE / 更新
move移動
copy複製
test更新前チェック

通常のAPI更新で特によく利用するのは、

  • add
  • remove
  • replace

の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

是非フォローしてください

最新の情報をお伝えします

類似投稿