前書き
についてキャッチアップしました。
今は 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 するのかをRFC6902
とRFC7386
が生まれてきました。
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-Type
とIf-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でテストしました。