JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7396) Explained

Two IETF standards describe changes to a JSON document instead of resending all of it. JSON Patch is a list of precise operations; JSON Merge Patch is a partial document that looks like the result. They solve the same problem with very different trade-offs.

Why patch formats exist

An HTTP PUT replaces a whole resource, so the client must send every field, including the ones it did not touch, and two clients editing different fields at the same time overwrite each other. The PATCH method (RFC 5789) sends only a description of the change. RFC 5789 does not say what that description looks like, so two formats became standard for JSON:

  • JSON Patch, RFC 6902, media type application/json-patch+json: an array of operations.
  • JSON Merge Patch, RFC 7396, media type application/merge-patch+json: a fragment merged into the target.

Both are also useful outside HTTP: audit logs, undo history, syncing configuration, and recording what changed between two versions of a file. The JSON diff tool shows such differences side by side, and the JSON Patch tool generates and applies patches in the browser.

JSON Patch operations

A JSON Patch document is an array. Each element is an object with an op member and a path, and some operations add value or from:

[
  { "op": "test",    "path": "/version", "value": 3 },
  { "op": "replace", "path": "/status", "value": "shipped" },
  { "op": "add",     "path": "/tags/-", "value": "priority" },
  { "op": "remove",  "path": "/draft" },
  { "op": "move",    "from": "/shipping/old", "path": "/shipping/address" },
  { "op": "copy",    "from": "/billing/email", "path": "/contact/email" }
]

The six operations:

  • add inserts a value. On an object it creates the member, or replaces it if it already exists. On an array it inserts at the index, shifting later elements right; the special index - appends.
  • remove deletes the member or array element. The target must exist.
  • replace swaps in a new value. The target must exist, which is the main difference from add.
  • move removes the value at from and adds it at path. A value cannot be moved into one of its own children.
  • copy adds a copy of the value at from to path.
  • test checks that the value at path equals value. Equality is structural: numbers compare by value, and objects match regardless of member order.

Operations run in order, each against the result of the previous one, and the patch is all or nothing: if any operation fails, including a failed test, the whole patch must be rejected and the document left unchanged. That makes test a cheap form of optimistic locking. In the example above, the update only applies if the version is still 3.

Paths are JSON Pointers

The path and from members use JSON Pointer (RFC 6901). A pointer is a string of reference tokens, each starting with /. The empty string "" points at the whole document; /items/0/sku means member items, element 0, member sku.

Two characters need escaping inside a token: ~ is written ~0 and / is written ~1. So a key named a/b is addressed as /a~1b, and m~n as /m~0n. Decode ~1 before ~0, otherwise ~01 turns into / instead of the correct ~1.

Array indexes are written in decimal without leading zeros; /items/01 is not a valid index. Pointers never use wildcards or filters, unlike JSONPath; if you need “every item whose price is zero”, select the items first with a JSONPath expression and then write one operation per match.

JSON Merge Patch

A merge patch is just JSON that looks like the parts of the target you want to change:

{
  "status": "shipped",
  "draft": null,
  "shipping": { "carrier": "DHL" }
}

Applied to an order, this sets status, deletes draft, and sets shipping.carrier while keeping the other members of shipping. The algorithm is short: if the patch is an object, merge it member by member, recursing into objects; a null value removes the member; anything that is not an object, including arrays, replaces the target value wholesale.

That simplicity has three consequences:

  1. You cannot set a value to null. Null always means “remove”. APIs that need explicit nulls must use JSON Patch.
  2. Arrays are replaced, not merged. To append one tag you must send the complete new array, so two clients adding tags at the same time will lose one of the additions.
  3. No conditions. There is no equivalent of test, so concurrency control has to come from HTTP, for example If-Match with an ETag.

A patch that is not an object at all, such as [1, 2] or "text", simply replaces the whole document.

Choosing between them

Use Merge Patch for typical form-style updates of a few scalar fields on a resource whose fields are never meaningfully null. It is readable, easy to produce from a form, and easy to validate with the same JSON Schema as the resource, with every field optional.

Use JSON Patch when you need any of the following: changing individual array elements, appending without resending the array, explicit nulls, moving data, or guarding the change with test. It is also the better format for storing history, because every operation is explicit and can be inverted if you record the old values.

Some APIs accept both and decide by Content-Type. Whatever you choose, document it, and reject patches with the wrong media type rather than guessing: a merge patch sent as application/json-patch+json fails, because an object is not an array of operations.

Common mistakes

  • Using dots instead of slashes. "path": "address.city" addresses a single key literally named address.city. Write /address/city.
  • Forgetting the leading slash. "path": "status" is not a valid pointer; it must be /status.
  • replace on a missing member. Fails by design. Use add if the member may not exist yet.
  • Index drift. After remove at /items/1, the old /items/2 becomes /items/1. When removing several elements, go from the highest index down.
  • Assuming partial success. A conforming implementation applies all operations or none. If your library keeps going after an error, it is not following RFC 6902.
  • Treating Merge Patch null as a value. Sending {"middleName": null} deletes the field; it does not store null.

Before sending a patch to a production API, check both the patch and the document it targets with the JSON validator, and keep the output of a dry run for review.

Frequently asked questions

What is the difference between add and replace in JSON Patch?

On an object, add creates the member or overwrites it if present, while replace fails unless the member already exists. On arrays, add inserts and shifts later elements, while replace overwrites the element at that index.

How do I append to an array with JSON Patch?

Use add with the index “-”, for example {“op”: “add”, “path”: “/tags/-”, “value”: “new”}. The dash means one past the last element.

Can JSON Merge Patch set a field to null?

No. In a merge patch null means “remove this member”. If your API needs to store explicit nulls, accept JSON Patch, which can replace a value with null.

Is a JSON Patch applied atomically?

Yes. RFC 6902 requires the whole patch to fail if any operation fails, including a failed test, so the target is never left half-modified.

Which Content-Type should a PATCH request use?

application/json-patch+json for JSON Patch and application/merge-patch+json for JSON Merge Patch. Plain application/json does not say which semantics apply.

Related