什麼?修改 JSON 內容居然還有個 JSON PATCH 標準

rife發表於2023-04-10

引言

你一定知道 JSON 吧,那專門用於修改 JSON 內容的 JSON PATCH 標準你是否知道呢?

RFC 6902 就定義了這麼一種 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 操作的不同,需要對應另外宣告 pathvaluefrom 欄位。

再例如,

原始 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

這些操作我相信不用做任何說明你就能理解其具體的含義,唯一要說明的可能就是 testtest 操作其實就是檢查 path 位置的值與 value 的值“相等”。

add

add 操作會根據 path 定義去執行不同的操作:

  • 如果 path 是一個陣列 index ,那麼新的 value 值會被插入到執行位置。
  • 如果 path 是一個不存在的物件成員,那麼新的物件成員會被新增到該物件中。
  • 如果 path 是一個已經存在的物件成員,那麼該物件成員的值會被 value 所替換。

add 操作必須另外宣告 pathvalue

path 目標位置必須是以下之一:

  • 目標文件的根 - 如果 path 指向的是根,那麼 value 值就將是整個文件的內容。
  • 一個已存在物件的成員 - 應用後 value 將會被新增到指定位置,如果成員已存在則其值會被替換。
  • 一個已存在陣列的元素 - 應用後 value 值會被新增到陣列中的指定位置,任何在指定索引位置或之上的元素都會向右移動一個位置。指定的索引不能大於陣列中元素的數量。可以使用 - 字元來索引陣列的末尾。

由於此操作旨在新增到現有物件和陣列中,因此其目標位置通常不存在。儘管指標的錯誤處理演算法將被呼叫,但本規範定義了 add 指標的錯誤處理行為,以忽略該錯誤並按照指定方式新增值。

然而,物件本身或包含它的陣列確實需要存在,並且如果不是這種情況,則仍然會出錯。

例如,對資料 { "a": { "foo": 1 } } 執行 add 操作,path 為 "/a/b" 時不是錯誤。但如果對資料 { "q": { "bar": 2 } } 執行同樣的操作則是一種錯誤,因為 "a" 不存在。


示例:

  1. add 一個物件成員

    # 源資料:
    { "foo": "bar"}
    
    # JSON Patch:
    [
        { "op": "add", "path": "/baz", "value": "qux" }
    ]
    
    # 結果:
    {
        "baz": "qux",
        "foo": "bar"
    }
  2. add 一個陣列元素

    # 源資料:
    { "foo": [ "bar", "baz" ] }
    
    # JSON Patch:
    [
        { "op": "add", "path": "/foo/1", "value": "qux" }
    ]
    
    # 結果:
    { "foo": [ "bar", "qux", "baz" ] }
  3. add 一個巢狀成員物件

    # 源資料:
    { "foo": "bar" }
    
    # JSON Patch:
    [
        { "op": "add", "path": "/child", "value": { "grandchild": { } } }
    ]
    
    # 結果:
    {
        "foo": "bar",
        "child": {
            "grandchild": {}
        }
    }
  4. 忽略未識別的元素

    # 源資料:
    { "foo": "bar" }
    
    # JSON Patch:
    [
        { "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
    ]
    
    # 結果:
    {
        "foo": "bar",
        "baz": "qux"
    }
  5. add 到一個不存在的目標失敗

    # 源資料:
    { "foo": "bar" }
    
    # JSON Patch:
    [
        { "op": "add", "path": "/baz/bat", "value": "qux" }
    ]
    
    # 失敗,因為操作的目標位置既不引用文件根,也不引用現有物件的成員,也不引用現有陣列的成員。
  6. add 一個陣列

    # 源資料:
    { "foo": ["bar"] }
    
    # JSON Patch:
    [
        { "op": "add", "path": "/foo/-", "value": ["abc", "def"] }
    ]
    
    # 結果:
    { "foo": ["bar", ["abc", "def"]] }

remove

remove 將會刪除 path 目標位置上的值,如果 path 指向的是一個陣列 index ,那麼右側其餘值都將左移。

示例:

  1. remove 一個物件成員

    # 源資料:
    {
        "baz": "qux",
        "foo": "bar"
    }
    
    # JSON Patch:
    [
        { "op": "remove", "path": "/baz" }
    ]
    
    # 結果:
    { "foo": "bar" }
  2. remove 一個陣列元素

    # 源資料:
    { "foo": [ "bar", "qux", "baz" ] }
    
    # JSON Patch:
    [
        { "op": "remove", "path": "/foo/1" }
    ]
    
    # 結果:
    { "foo": [ "bar", "baz" ] }

replace

replace 操作會將 path 目標位置上的值替換為 value。此操作與 removeadd 同樣的 path 在功能上是相同的。

示例:

  1. replace 某個值

    # 源資料:
    {
        "baz": "qux",
        "foo": "bar"
    }
    
    # JSON Patch:
    [
        { "op": "replace", "path": "/baz", "value": "boo" }
    ]
    
    # 結果:
    {
        "baz": "boo",
        "foo": "bar"
    }

move

move 操作將 from 位置的值移動到 path 位置。from 位置不能是 path 位置的字首,也就是說,一個位置不能被移動到它的子級中。

示例:

  1. 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"
        }
    }
  2. 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):如果它們完全一樣,則被視為相等。

請注意,所進行的比較是邏輯比較;例如,陣列成員之間的空格不重要。

示例:

  1. test 某個值成功

    # 源資料:
    {
        "baz": "qux",
        "foo": [ "a", 2, "c" ]
    }
    
    # JSON Patch:
    [
        { "op": "test", "path": "/baz", "value": "qux" },
        { "op": "test", "path": "/foo/1", "value": 2 }
    ]
  2. test 某個值錯誤

    # 源資料:
    { "baz": "qux" }
    
    # JSON Patch:
    [
        { "op": "test", "path": "/baz", "value": "bar" }
    ]
  3. ~ 符號轉義

    ~ 字元是 JSON 指標中的關鍵字。因此,我們需要將其編碼為 〜0

    # 源資料:
    {
        "/": 9,
        "~1": 10
    }
    
    # JSON Patch:
    [
        {"op": "test", "path": "/~01", "value": 10}
    ]
    
    # 結果:
    {
        "/": 9,
        "~1": 10
    }
  4. 比較字串和數字

    # 源資料:
    {
        "/": 9,
        "~1": 10
    }
    
    # JSON Patch:
    [
        {"op": "test", "path": "/~01", "value": "10"}
    ]
    
    # 失敗,因為不遵循上述相等的規則。

結語

使用 JSON PATCH 的原因之一其實是為了避免在只需要修改某一部分內容的時候重新傳送整個文件。JSON PATCH 也早已應用在了 Kubernetes 等許多專案中。

相關文章