← 全部文章

JSON Patch vs JSON Merge Patch:RFC 6902 vs 7396

JSON Patch (RFC 6902) 发送显式操作;JSON Merge Patch (RFC 7396) 叠加一个部分对象。看示例对比并选择合适的那一种。

当一个 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 PatchJSON Patch
RFC73966902
Content-Typeapplication/merge-patch+jsonapplication/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-Match ETag 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 只能把整个数组替换掉。

相关工具与阅读