当一个 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