LoginSignup
10
5

More than 1 year has passed since last update.

JSON Patchをキャッチアップしました

Posted at

前書き

についてキャッチアップしました。

今は API デザインしているので、PATCHに関する操作であれば、
上記のJSON PATCHともう一つ、JSON Merge Patchがあります。

今回はJSON PATCHについてフォーカスしてキャッチアップしました。

由来

JSON RFCは JSON を定義しました。
また、PATCH Method for HTTPは HTTP の PATCH メソッドを定義しました。

もともと PATCH 以外に、PUT というメソッドがあり、
PUT の場合にリソースに対して全て更新するというふうに使われています。
PATCH は PUT と違って、リソースに対して一部を更新することが可能と分かります。

RFC5789では PATCH を定義しましたが、具体的なフォーマットに対してどういうふうに操作するべきかを定義していませんでした。
例えばXML Patchとかもあります。

RFC5789の補足として、JSON データをどうやって PATCH するのかをRFC6902RFC7386が生まれてきました。

JSON Pointer

Patch は JSON ドキュメントの一部を更新できるので、
じゃあどうやってその一部を特定できるのかについては、JSON Pointerという概念が出てきました。

""はルートを意味しています。
それ以外に、/を使ってオブジェクトのプロパティを区切って検索します。

例えば:

{
  "a": {
    "b": 1,
    "c": [2, 3]
  },
  "d": 4
}
""         全部のドキュメント
"/a"       { "b": 1, "c": [2, 3] }
"/d"       4
"/a/b"     1
"/a/c"     [2, 3]
"/a/c/1"   3

これでリソース内の任意の部分を特定できるようになりました。

特殊な文字列の処理

後は、
/とか˜は特殊な意味を持っているので、エスケープ処理が必要です。

{
  "a/b": 1,
  "c˜d": {
    "e": [
      2,
      {
        "f/g": 3
      }
    ]
  }
}

それぞれを取得した場合に、

"a˜1b"            1
"c˜0d/e"          [2, { "f/g": 3 }]
"c˜0d/e/1/f˜g"    3

というふうにエスケープが必要にあります。

  • ˜ => ˜0
  • / => ˜1

JSON Patch

というわけで、ドキュメント内の操作したい部分を特定できたので、
次に実際どういう風に操作できるのかを見てみましょう。

Document Structure

まず実際のリクエストのデータストラクチャーを見てみましょう。

 PATCH /my/data HTTP/1.1
 Host: example.org
 Content-Length: 326
 Content-Type: application/json-patch+json
 If-Match: "abc123"
 [
   { "op": "test", "path": "/a/b/c", "value": "foo" },
   { "op": "remove", "path": "/a/b/c" },
   { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
   { "op": "replace", "path": "/a/b/c", "value": 42 },
   { "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
   { "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
 ]

上記は RFC 内で提供している例です。

ちょっとここで注目してもらいたいのは、Content-TypeIf-Matchの部分です。

  • Content-Type: application/json-patch+json
  • If-Match: 文字列、ID など

もし Content-Type が間違った場合に415を返します。
If-Matchについてなぜ必要なのかはRFC5789の中に詳細を書いてありますが、
一言で言いますと、
同時に複数個の PATCH 操作が発生する可能性がありますので、
空中衝突を防止するために、
If-Matchをつけるのがお勧めですね。
詳細はもっとキャッチアップして記事を書いてみようと思います。

add

例で説明します。

// 既存のデータ
{
  "a": 1,
  "b": [2, 3],
  "c": {
    "d": 4
  }
}
// patch
[
  { "op": "add", "path": "/b/-", "value": 5 }, // -は最後を意味しています
  { "op": "add", "path": "/c/e", "value": 6 },
  { "op": "add", "path": "/f", "value": 7 }
  { "op": "add", "path": "g", "value": 7 } // これはエラーになる、パスが存在しないため
]
// result
{
  "a": 1,
  "b": [2, 3, 5],
  "c": { "d": 4, "e": 6 },
  "f": 7
}

replace

// 既存のデータ
{
  "a": 1,
  "b": [2, 3],
  "c": {
    "d": 4
  }
}
// patch
[
  { "op": "replace", "path": "/a", "value": 10 },
  { "op": "replace", "path": "/b/1", "value": 30 },
  { "op": "replace", "path": "/c/d", "value": 40 },
  { "op": "replace", "path": "/c/e", "value": 50 } // 存在しないパスのため、エラーが発生する
]
// result
{
  "a": 10,
  "b": [2, 30],
  "c": { "d": 40, "e": 50 }
}

remove

// 既存のデータ
{
  "a": 1,
  "b": [2, 3],
  "c": {
    "d": 4
  }
}
[
  { "op": "remove", "path": "/a" },
  { "op": "remove", "path": "/b/1" },
  { "op": "remove", "path": "/b/99" }, // 存在しないパスのため、エラーが発生する
  { "op": "remove", "path": "/c/d" }
  { "op": "remove", "path": "/c/e" } // 存在しないパスのため、エラーが発生する
]
{
  "b": [2],
  "c": {}
}

move

// 既存のデータ
{
  "a": 1,
  "b": [2, 3],
  "c": {
    "d": 4
  }
}
[
  { "op": "move", "from": "/a", "path": "/e" },
  { "op": "move", "from": "/b/0", "path": "/f" },
  { "op": "move", "from": "/c", "path": "/g" },
  { "op": "move", "from": "/c", "path": "/g" } // /cがもうすでに存在しないためエラーが発生する
]
{
  "b": [3],
  "e": 1,
  "f": 2,
  "g": { "d": 4 }
}

copyについては割愛する。

test

// 既存のデータ
{
  "a": 1,
  "b": [2, 3],
  "c": {
    "d": 4
  }
}
[
  {
    "op": "test",
    "path": "/b/0",
    "value": 2
  }
]

path に対する値が期待値と同じであるかどうかをテストする。

最後に

上記のコードを全部JSON-Patchでテストしました。

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5