修改 JSON 的 JSON PATCH 標準
JSON PATCH
JSON Patch 本身也是一種 JSON 文檔結構,用于表示要應用于 JSON 文檔的操作序列;它適用于 HTTP PATCH 方法,其 MIME 媒體類型為 "application/json-patch+json"。
這句話也許不太好理解,我們先看一個例子:
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" }
]
這個 HTTP 請求的 body 也是 JSON 格式(JSON PATCH 本身也是一種 JSON 結構),但是這個 JSON 格式是有具體規范的(只能按照標準去定義要應用于 JSON 文檔的操作序列)。
具體而言,JSON Patch 的數據結構就是一個 JSON 對象數組,其中每個對象必須聲明 op 去定義將要執行的操作,根據 op 操作的不同,需要對應另外聲明 path、value 或 from 字段。
再例如,
原始 JSON :
{
"a": "aaa",
"b": "bbb"
}
應用如下 JSON PATCH :
[
{ "op": "replace", "path": "/a", "value": "111" },
{ "op": "remove", "path": "/b" }
]
得到的結果為:
{
"a": "111"
}
需要注意的是:
- patch 對象中的屬性沒有順序要求,比如 { "op": "remove", "path": "/b" } 與 { "path": "/b", "op": "remove" } 是完全等價的。
- patch 對象的執行是按照數組順序執行的,比如上例中先執行了 replace,然后再執行 remove。
- patch 操作是原子的,即使我們聲明了多個操作,但最終的結果是要么全部成功,要么保持原數據不變,不存在局部變更。也就是說如果多個操作中的某個操作異常失敗了,那么原數據就不變。
op
op 只能是以下操作之一:
- add
- remove
- replace
- move
- copy
- test
這些操作我相信不用做任何說明你就能理解其具體的含義,唯一要說明的可能就是 test,test 操作其實就是檢查 path 位置的值與 value 的值“相等”。
add
add 操作會根據 path 定義去執行不同的操作:
- 如果 path 是一個數組 index ,那么新的 value 值會被插入到執行位置。
- 如果 path 是一個不存在的對象成員,那么新的對象成員會被添加到該對象中。
- 如果 path 是一個已經存在的對象成員,那么該對象成員的值會被 value 所替換。
add 操作必須另外聲明 path 和 value。
path 目標位置必須是以下之一:
- 目標文檔的根-如果 path 指向的是根,那么 value 值就將是整個文檔的內容。
- 一個已存在對象的成員 - 應用后 value 將會被添加到指定位置,如果成員已存在則其值會被替換。
- 一個已存在數組的元素 - 應用后 value 值會被添加到數組中的指定位置,任何在指定索引位置或之上的元素都會向右移動一個位置。指定的索引不能大于數組中元素的數量。可以使用 - 字符來索引數組的末尾。
由于此操作旨在添加到現有對象和數組中,因此其目標位置通常不存在。盡管指針的錯誤處理算法將被調用,但本規范定義了 add 指針的錯誤處理行為,以忽略該錯誤并按照指定方式添加值。
然而,對象本身或包含它的數組確實需要存在,并且如果不是這種情況,則仍然會出錯。
例如,對數據 { "a": { "foo": 1 } } 執行 add 操作,path 為 "/a/b" 時不是錯誤。但如果對數據 { "q": { "bar": 2 } } 執行同樣的操作則是一種錯誤,因為 "a" 不存在。
示例:
- add 一個對象成員
# 源數據:
{ "foo": "bar"}
# JSON Patch:
[
{ "op": "add", "path": "/baz", "value": "qux" }
]
# 結果:
{
"baz": "qux",
"foo": "bar"
}
- add 一個數組元素
# 源數據:
{ "foo": [ "bar", "baz" ] }
# JSON Patch:
[
{ "op": "add", "path": "/foo/1", "value": "qux" }
]
# 結果:
{ "foo": [ "bar", "qux", "baz" ] }
- add 一個嵌套成員對象
# 源數據:
{ "foo": "bar" }
# JSON Patch:
[
{ "op": "add", "path": "/child", "value": { "grandchild": { } } }
]
# 結果:
{
"foo": "bar",
"child": {
"grandchild": {}
}
}
- 忽略未識別的元素
# 源數據:
{ "foo": "bar" }
# JSON Patch:
[
{ "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
]
# 結果:
{
"foo": "bar",
"baz": "qux"
}
- add 到一個不存在的目標失敗
# 源數據:
{ "foo": "bar" }
# JSON Patch:
[
{ "op": "add", "path": "/baz/bat", "value": "qux" }
]
# 失敗,因為操作的目標位置既不引用文檔根,也不引用現有對象的成員,也不引用現有數組的成員。
- add 一個數組
# 源數據:
{ "foo": ["bar"] }
# JSON Patch:
[
{ "op": "add", "path": "/foo/-", "value": ["abc", "def"] }
]
# 結果:
{ "foo": ["bar", ["abc", "def"]] }
remove
remove 將會刪除 path 目標位置上的值,如果 path 指向的是一個數組 index ,那么右側其余值都將左移。
示例:
- remove 一個對象成員
# 源數據:
{
"baz": "qux",
"foo": "bar"
}
# JSON Patch:
[
{ "op": "remove", "path": "/baz" }
]
# 結果:
{ "foo": "bar" }
- remove 一個數組元素
# 源數據:
{ "foo": [ "bar", "qux", "baz" ] }
# JSON Patch:
[
{ "op": "remove", "path": "/foo/1" }
]
# 結果:
{ "foo": [ "bar", "baz" ] }
replace
replace 操作會將 path 目標位置上的值替換為 value。此操作與 remove 后 add 同樣的 path 在功能上是相同的。
示例:
- replace 某個值
# 源數據:
{
"baz": "qux",
"foo": "bar"
}
# JSON Patch:
[
{ "op": "replace", "path": "/baz", "value": "boo" }
]
# 結果:
{
"baz": "boo",
"foo": "bar"
}
move
move 操作將 from 位置的值移動到 path 位置。from 位置不能是 path 位置的前綴,也就是說,一個位置不能被移動到它的子級中。
示例:
- move 某個值
# 源數據:
{
"foo": {
"bar": "baz",
"waldo": "fred"
},
"qux": {
"corge": "grault"
}
}
# JSON Patch:
[
{ "op": "move", "from": "/foo/waldo", "path": "/qux/thud" }
]
# 結果:
{
"foo": {
"bar": "baz"
},
"qux": {
"corge": "grault",
"thud": "fred"
}
}
- move 一個數組元素
# 源數據:
{ "foo": [ "all", "grass", "cows", "eat" ] }
# JSON Patch:
[
{ "op": "move", "from": "/foo/1", "path": "/foo/3" }
]
# 結果:
{ "foo": [ "all", "cows", "eat", "grass" ] }
copy
copy 操作將 from 位置的值復制到 path 位置。
test
test 操作會檢查 path 位置的值是否與 value “相等”。
這里,“相等”意味著 path 位置的值和 value 的值是相同的JSON類型,并且它們遵循以下規則:
- 字符串:如果它們包含相同數量的 Unicode 字符并且它們的碼點是逐字節相等,則被視為相等。
- 數字:如果它們的值在數值上是相等的,則被視為相等。
- 數組:如果它們包含相同數量的值,并且每個值可以使用此類型特定規則將其視為與另一個數組中對應位置處的值相等,則被視為相等。
- 對象:如果它們包含相同數量的成員,并且每個成員可以通過比較其鍵(作為字符串)和其值(使用此類型特定規則)來認為與其他對象中的成員相等,則被視為相等 。
- 文本(false,true 和 null):如果它們完全一樣,則被視為相等。
請注意,所進行的比較是邏輯比較;例如,數組成員之間的空格不重要。
示例:
- test 某個值成功
# 源數據:
{
"baz": "qux",
"foo": [ "a", 2, "c" ]
}
# JSON Patch:
[
{ "op": "test", "path": "/baz", "value": "qux" },
{ "op": "test", "path": "/foo/1", "value": 2 }
]
- test 某個值錯誤
# 源數據:
{ "baz": "qux" }
# JSON Patch:
[
{ "op": "test", "path": "/baz", "value": "bar" }
]
- ~ 符號轉義
~ 字符是 JSON 指針中的關鍵字。因此,我們需要將其編碼為 ?0
# 源數據:
{
"/": 9,
"~1": 10
}
# JSON Patch:
[
{"op": "test", "path": "/~01", "value": 10}
]
# 結果:
{
"/": 9,
"~1": 10
}
- 比較字符串和數字
# 源數據:
{
"/": 9,
"~1": 10
}
# JSON Patch:
[
{"op": "test", "path": "/~01", "value": "10"}
]
# 失敗,因為不遵循上述相等的規則。
使用 JSON PATCH 的原因之一其實是為了避免在只需要修改某一部分內容的時候重新發送整個文檔。JSON PATCH 也早已應用在了 Kubernetes 等許多項目中。
在k8s中 kubectl patch 命令允許用戶對運行在 Kubernetes 集群中的資源進行局部更新。相較于我們經常使用的 kubectl apply 命令,kubectl patch 命令在更新時無需提供完整的資源文件,只需要提供要更新的內容即可。
Kubectl patch 支持以下 3 種 patch 類型:
- strategic patch(默認):根據不同字段 patchStrategy 決定具體的合并 patch 策略。Strategic merge patch 并非通用的 RFC 標準,而是 Kubernetes 特有的一種更新 Kubernetes 資源對象的方式。與 JSON merge patch 和 JSON patch 相比,strategic merge patch 更為強大。
- JSON merge patch:遵循 JSON Merge Patch, RFC 7386[1] 規范,根據 patch 中提供的期望更改的字段及其對應的值,更新到目標中。
- JSON patch:遵循 JSON Patch, RFC 6902[2] 規范,通過明確的指令表示具體的操作。