Use JSON Merge Patch when clients send simple partial updates to object fields and your model has no meaningful nulls. Use JSON Patch when you need array-element updates, real nulls, moves, or the test operation for optimistic concurrency. Merge Patch is what most people mean by "send a PATCH"; JSON Patch is what you graduate to when the edge cases arrive.
§1 · The same change, both formats
Original document: {"name": "Aurora", "status": "active", "config": {"retries": 3}, "tags": ["edge", "beta"]}. We want: status → "paused", retries → 5, drop the whole config.sampleRate idea, and change the first tag.
JSON Patch (RFC 6902) — an ordered operation list
| Op | Meaning |
|---|---|
| {"op":"replace", "path":"/status", "value":"paused"} | exact field, exact intent |
| {"op":"replace", "path":"/config/retries", "value":5} | nested path, one key only |
| {"op":"replace", "path":"/tags/0", "value":"ga"} | array element BY INDEX — Merge Patch cannot do this |
| {"op":"test", "path":"/status", "value":"active"} | abort atomically if someone changed it first |
JSON Merge Patch (RFC 7386) — the shape of the result
| Patch | Effect |
|---|---|
| {"status":"paused", "config":{"retries":5}} | fields mentioned are set; fields omitted stay |
| {"owner": null} | ⚠ null means DELETE the field — not "set to null" |
| {"tags": ["ga","beta"]} | arrays replace WHOLE — there is no per-element edit |
§2 · The decision table
| Need | Merge Patch | JSON Patch |
|---|---|---|
| Simple field updates | perfect | works, verbose |
| Set a field to real null | impossible (null = delete) | yes |
| Edit one array element | no (whole-array replace) | yes, by index |
| Optimistic concurrency | no | test op |
| Move / copy values | no | yes |
| Human-writable by hand | yes | painful |
| Content type | application/merge-patch+json | application/json-patch+json |
Generate either format from two JSONs: paste before/after and export an applicable RFC 6902 Patch or RFC 7386 Merge Patch.
Open JSON diff tool →§3 · The gotchas that reach production
- Merge Patch null deletes. A client that serializes "unset optional fields as null" will silently strip fields from your documents. If your ORM emits nulls, you have an incident waiting.
- Arrays replace whole in Merge Patch. Two clients "adding a tag" concurrently = last write wins, one tag lost. Array-append semantics need JSON Patch's
{"op":"add","path":"/tags/-"}. - JSON Patch paths escape special characters:
~is~0and/is~1. A key nameda/bis addressed as/a~1b— hand-written patches get this wrong constantly. - JSON Patch is ordered and atomic. Operations apply in sequence; if any fails (including
test), the whole patch MUST fail. A remove that shifts array indexes changes what the next op points at — order your ops as if they execute one by one, because they do. - Don't accept both on one endpoint blindly. Dispatch by
Content-Type— the two formats are not distinguishable by shape in every case (a Merge Patch could look like a weird object; an operation array is unambiguous, but the reverse isn't).
§4 · FAQ
What's the difference in one sentence?
JSON Patch says what to do (ops on paths, ordered, atomic); Merge Patch says what it should look like (partial result document).
How do I set null with Merge Patch?
You can't — null means delete. Real nulls in your model are the classic forcing function toward JSON Patch.
How do I append to an array with JSON Patch?
{"op":"add","path":"/tags/-","value":"new"} — the - index means "end of array".
Which content types do I use?
application/json-patch+json for RFC 6902, application/merge-patch+json for RFC 7386 — and dispatch on them server-side.