當一個 HTTP API 需要更新資源的 一部分 時,有兩個 IETF 標準在競爭這份工作:JSON Merge Patch(RFC 7396)與 JSON Patch(RFC 6902)。它們看起來相似,運作方式卻很不一樣 —— Merge Patch 把一個局部物件疊上去,而 JSON Patch 送一份明確的操作清單。本指南說明各自怎麼運作、並排對比,並協助你挑對的那一個。
問題:局部更新
一個 PUT 請求會把整個資源替換掉,在你只想改一個欄位時既浪費又有風險。HTTP PATCH 方法就是為局部更新而生 —— 但 PATCH 沒有定義 body 格式。這兩個標準就是要填這個坑。
JSON Merge Patch(RFC 7396)
Merge Patch 就是目標文件的一份局部版本。伺服器把它疊上去:patch 裡有的 key 被設定,被設為 null 的 key 被 刪除,沒提到的 key 維持原樣。
// 原始
{ "title": "Hello", "author": "Ada", "tags": ["a", "b"], "draft": true }
// Merge Patch body (Content-Type: application/merge-patch+json)
{ "title": "Hello World", "draft": null }
// 結果
{ "title": "Hello World", "author": "Ada", "tags": ["a", "b"] }
// ^ title 被更新 ^ author 不動 ^ draft 被刪(先前是 null) 直觀又緊湊。代價:因為 null 意味著「刪」,你 無法用 Merge Patch 把一個值設成 null;陣列也只能整個替換 —— 沒辦法改某一個陣列元素。
JSON Patch(RFC 6902)
一個 JSON Patch 是一份有序的操作陣列,每個操作以 JSON Pointer 路徑指定位置。明確而精準:
// JSON Patch body (Content-Type: application/json-patch+json)
[
{ "op": "replace", "path": "/title", "value": "Hello World" },
{ "op": "remove", "path": "/draft" },
{ "op": "add", "path": "/tags/-", "value": "c" },
{ "op": "test", "path": "/author", "value": "Ada" }
]六種操作:
| op | 效果 |
|---|---|
add | 新增一個值(或用 /- 追加到陣列尾) |
remove | 刪除路徑上的值 |
replace | 替換路徑上的值 |
move | 把一個值從一個路徑搬到另一個 |
copy | 把一個值複製到另一個路徑 |
test | 斷言一個值(失敗則整個 patch 中止) |
test 操作讓安全的原子更新成為可能:若文件不在預期狀態,整個 patch 都會被拒 —— 對樂觀並發控制很有用。
正面對比
| JSON Merge Patch | JSON Patch | |
|---|---|---|
| RFC | 7396 | 6902 |
| Content-Type | application/merge-patch+json | application/json-patch+json |
| 可讀性 | 高 —— 看起來像資料 | 較低 —— 是操作清單 |
| 可把值設為 null | 不可(null 表示刪除) | 可 |
| 修改單個陣列元素 | 不可(要整個陣列替換) | 可(依索引) |
| 條件 / 原子更新 | 不可 | 可(test 操作) |
| Move / copy | 不可 | 可 |
| 最適合 | 簡單的物件欄位更新 | 精準、關心陣列、原子的編輯 |
什麼時候用哪一個
- JSON Merge Patch —— 當客戶端主要更新頂層物件欄位,且你看重「body 讀起來像資源」時。簡單設定與個人檔更新很合適。記住:你不能把欄位設為
null也不能編輯陣列項。 - JSON Patch —— 當你需要編輯陣列元素、搬移或複製值、把欄位設為
null,或在前置條件下原子地套用變更時。協作編輯與便於稽核的變更集的標準選擇。
HTTP PATCH 語意:冪等與 header
團隊上線 PATCH endpoint 時常踩的兩個 HTTP 細節:
- PATCH 不必冪等(不像 PUT)。一個帶
{"op":"add","path":"/items/-","value":...}的 JSON Patch 是追加 —— 套用兩次就追加兩次。若想安全重試,要嘛先用test操作斷言狀態,要嘛把操作設計成「重複套用是無動作」(對固定路徑用replace而非對/items/-做add)。 - 用對 Content-Type。 RFC 6902 =
application/json-patch+json;RFC 7396 =application/merge-patch+json。伺服器通常依此分支。 - 樂觀並 發。 把 JSON Patch 的
test操作(或If-MatchETag header)搭配起來,並發寫入者就無法默默互相覆寫。
值得認識的函式庫
JSON Patch 這邊,fast-json-patch 是事實上的 npm 套件 —— 可以 apply、generate(對兩份文件做 diff 生成 patch)、observe(追蹤被監看物件的變化)。JSON Merge Patch 的規則小到可以內聯手寫,但 json-merge-patch 把那些 null-刪除邊界條件都處理好了。
在程式碼裡套用 patch
// JavaScript —— fast-json-patch 實作 RFC 6902
import { applyPatch } from 'fast-json-patch';
const updated = applyPatch(document, patchOps).newDocument;
// JSON Merge Patch 用遞迴就很容易套用,但函式庫
// (例如 json-merge-patch)會正確處理 null-刪除規則。套用 patch 之後,可以對比 before/after 確認改動正是你想要的 —— 見 如何對比兩份 JSON 檔案,或把兩份都貼到 JSON Diff。
常見問題
JSON Patch 與 JSON Merge Patch 有何差異?
JSON Merge Patch(RFC 7396)是文件的局部副本,其中的 null 用來刪 key。JSON Patch(RFC 6902)是明確的操作陣列(add、remove、replace、move、copy、test),以 JSON Pointer 路徑為目標。Merge Patch 較簡單;JSON Patch 較強大。
為什麼 JSON Merge Patch 不能把欄位設為 null?
因為在 Merge Patch 裡 null 是 刪除 一個 key 的信號。若你需要存真正的 null 值,用 JSON Patch 配 replace 操作。
我該用哪個 Content-Type?
application/merge-patch+json 用於 Merge Patch;application/json-patch+json 用於 JSON Patch,都以 HTTP PATCH 方法傳送。
怎麼編輯陣列中的一個元素?
用 JSON Patch 加上像 /items/2 的路徑,或用 /items/- 追加。Merge Patch 只能把整個陣列替換。
相關工具與閱讀
- JSON Diff —— 驗證一個 patch 是否產生你預期的變更
- 如何對比兩份 JSON 檔案 —— 語意 diff 背後的演算法
- RFC 7396:JSON Merge Patch
- RFC 6901 與 6902:JSON Pointer 與 Patch