Skip to content
Back to Blog
By JSONConvert Team··7 min read

JSON Patch vs JSON Merge Patch Explained

You want to change one field on a resource, so you reach for HTTP PATCH. Then you have to decide what the request body looks like, and there are two standards for it: JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7396). They look nothing alike, they fail in different ways, and picking the wrong one usually shows up later as "why can't I set this field to null?" This post shows how each format works, where each one breaks, and how to apply them in JavaScript, Python and kubectl.

The same change, two ways

Start with a user record from an API:

You want to rename the user, drop the beta flag, and add an admin role.

As a JSON Merge Patch (Content-Type: application/merge-patch+json):

As a JSON Patch (Content-Type: application/json-patch+json):

The merge patch is a partial copy of the document. The JSON Patch is a list of instructions. That difference drives everything else.

How JSON Merge Patch works

The rules fit in one paragraph. Walk the patch object. For each key, if the value is null, delete that key from the target. If the value is an object, recurse into it. Anything else, including an array, replaces the target value outright. If the patch itself is not an object, it replaces the whole document.

Here is the full algorithm in TypeScript. It is short enough that you rarely need a library:

Two consequences catch people out.

First, you cannot set a field to null. {"middleName": null} means "delete middleName", not "store null". If your API treats absent and null as different states, merge patch cannot express the difference. We covered why that distinction matters in optional vs nullable JSON fields.

Second, arrays are all or nothing. To add one role you have to send the whole array, which means you need to have read it first. If two clients each add a role at the same time, the second write silently drops the first one's change.

How JSON Patch works

A JSON Patch is an array of operations applied in order. There are six:

opWhat it doesExample
addInsert a value, or append to an array with -{"op":"add","path":"/roles/-","value":"admin"}
removeDelete a key or array element{"op":"remove","path":"/settings/beta"}
replaceOverwrite a value that must already exist{"op":"replace","path":"/name","value":"Ada Lovelace"}
moveRemove from one path, add at another{"op":"move","from":"/email","path":"/contact/email"}
copyDuplicate a value to another path{"op":"copy","from":"/name","path":"/displayName"}
testFail the whole patch unless the value matches{"op":"test","path":"/version","value":7}
Paths use JSON Pointer (RFC 6901), not JSONPath. /roles/0 is the first role, /settings/theme is a nested key. Two characters need escaping inside a key: ~ becomes ~0 and / becomes ~1. So a key named a/b is addressed as /a~1b. If you are used to $.settings.theme from JSONPath queries, note that JSON Pointer has no wildcards or filters. Every operation targets exactly one location.

The patch is atomic. If any operation fails, say a replace on a path that does not exist, the whole patch is rejected and the document stays unchanged. Combined with test, that gives you optimistic concurrency inside the request body:

If someone else bumped the version to 8 first, the test fails and nothing is written. Merge patch has no equivalent, so you would need an If-Match header and an ETag.

JSON Patch can also store an explicit null, because {"op":"replace","path":"/middleName","value":null} is unambiguous.

Which one to pick

JSON Merge PatchJSON Patch
Body shapePartial documentArray of operations
Set a field to nullNot possibleYes
Append to an arrayResend the whole arrayadd with /-
Atomic preconditionsNotest op
Readable by humansVeryFine for short patches
Easy to generate from a formYesNeeds a diff step
My default for public APIs is merge patch. Clients can build the body by copying the fields the user edited, and most resources do not need explicit nulls or array surgery. Switch to JSON Patch when you have large arrays that several clients modify, when null is a meaningful value, or when you need the test precondition. Most "partial update" endpoints you meet in the wild behave like merge patch. Kubernetes accepts both, plus its own strategic merge patch.

Applying patches in code

In JavaScript, fast-json-patch handles RFC 6902 and can also generate a patch by diffing two documents:

applyPatch mutates the document you pass in by default, which is why the example clones it first. Forgetting that is a common source of "my original object changed" bugs.

In Python, the jsonpatch package does the same, and returns a new document unless you ask for in-place:

For merge patch in Python, port the TypeScript function above. It is about ten lines and avoids a dependency.

With kubectl, the --type flag picks the format:

Without --type, kubectl uses strategic merge patch, which merges some lists by key (containers by name, for example) instead of replacing them. It only works on built-in Kubernetes types, not on custom resources, which is why --type=merge is the usual choice for CRDs.

Debugging a patch that did the wrong thing

When a patch produces an unexpected result, compare the document before and after rather than reading the patch. Paste both versions into the JSON diff tool and you get every added, removed and changed path. That usually shows the problem straight away: an array that was replaced instead of appended to, or a key that vanished because a merge patch sent null.

If the server rejects the body outright, check that it is valid JSON before blaming the patch logic. Run it through the JSON validator, then format it so each operation sits on its own line. A trailing comma after the last operation is the most common cause of a 400 here.

One last gotcha: replace requires the target to exist, while add on an existing object key quietly overwrites it. If you are not sure the field is present, use add. Use replace when a missing field should be an error.

Related Tools