
用户只改个人资料里的一个字段时,提交完整对象会增加网络开销和覆盖风险。部分更新 API 的关键,是把字段变化、数组处理和空值含义写成可执行的契约。本文比较 JSON Patch 与 JSON Merge Patch,并给出校验与并发控制方案。
两种格式解决不同问题
JSON Patch(RFC 6902)把更新表达为有序操作数组,每个操作通过 JSON Pointer 指向路径:
1 | [ |
它能区分新增、删除、替换、移动和复制,适合数组元素和原子条件检查;test 失败时,整个文档不应被修改。
JSON Merge Patch(RFC 7396)则使用一个普通 JSON 对象描述目标片段:
1 | {"displayName": "Lin", "bio": null} |
对象字段递归合并,null 表示删除,未出现的字段保持原值。它易于手写,但无法表达“替换数组第 2 项”;数组通常会整体替换。

| 需求 | JSON Patch | JSON Merge Patch |
|---|---|---|
| 修改数组单项 | 适合,路径可定位下标 | 通常只能整体替换 |
| 表达删除 | remove,语义明确 |
null,需要允许字段为空的约定 |
| 客户端可读性 | 操作较多,适合工具生成 | 结构接近资源对象 |
| 条件更新 | test 可放入同一补丁 |
需配合 If-Match 或业务版本号 |
先做媒体类型和结构校验
不要根据请求体长相猜格式。服务端应要求 Content-Type: application/json-patch+json 或 application/merge-patch+json,否则返回 415 Unsupported Media Type。解析后做三层校验:
- 语法层:JSON 是否有效,Patch 是否为数组,操作名和必填字段是否齐全。
- 路径层:只允许公开字段,拒绝
/roles、/ownerId等路径,并限制操作数、路径深度和请求体大小。 - 领域层:应用补丁后校验完整资源,例如邮箱格式、状态转换和字段不变量;校验放在写库事务内。
JSON Pointer 要正确处理 ~0、~1 转义,不能把路径拼成 SQL 或文件路径。数组下标检查边界,/- 只能用于 add。错误响应固定包含 code、path 和 message,便于定位操作。
把读取和写入绑定起来
部分更新仍会遇到“最后一次写入覆盖前一次”。让资源携带版本号或 ETag,客户端提交条件请求:
1 | PATCH /users/42 |
服务端在事务中检查版本,应用补丁并递增版本;不匹配返回 412 Precondition Failed,不产生副作用。test /version 可作第二道保护,但不能替代数据库条件更新:UPDATE ... WHERE id = ? AND version = ?,并检查受影响行数。

幂等、审计和上线清单
replace 同一个值通常幂等,add /labels/- 却会在重试时重复追加。可提供 Idempotency-Key 或保存操作编号做短期去重。审计记录操作者、媒体类型、原始补丁、旧版本和新版本,敏感字段脱敏。
上线前验证:删除不存在路径、数组越界、重复操作、空对象、超大补丁、权限不足、版本冲突和事务回滚。为两种媒体类型建立契约测试,确认错误码、响应 ETag 和重试行为稳定。这样,PATCH 才是可预测的更新协议。