Skip to content
APIs

JSON Patch vs JSON Merge Patch: Partial Updates Done Right

·8 min read
JSON Patch vs JSON Merge Patch: Partial Updates Done Right - APIs guide from JSON Prettifier

Quick answer: Both are standard bodies for HTTP PATCH. JSON Merge Patch (RFC 7396) is a fragment of the resource: send only the fields to change, and null deletes one. JSON Patch (RFC 6902) is an array of operations (add, remove, replace, move, copy, test) targeting JSON Pointer paths, applied atomically. Use Merge Patch for flat objects; use JSON Patch when you need to edit arrays, store null, or make the update conditional.

A user changes their display name. Your client sends the entire 2 KB profile object back with PUT, and the request races with another tab that changed the email address. One of the two edits is now gone. HTTP PATCH exists for exactly this: send the change, not the document. But PATCH does not say what the body looks like, and the two formats that fill that gap have different strengths.

Use JSON Compare to inspect added, removed, and changed values in two non-sensitive sample documents.

Why HTTP PATCH Needs a Body Format

RFC 5789 defines PATCH as "apply this set of changes" and deliberately leaves the format of the changes to the Content-Type header. That is why a PATCH with application/json is ambiguous: the server has to guess whether {"name": null} means "clear the name" or "set it to null". The two registered JSON formats remove the guesswork, and the media type tells the server which rules to apply. See the JSON Content-Type guide for how servers accept the +json variants.

PUT vs PATCH in one paragraph

PUT replaces the whole resource with the body you send; anything you leave out is gone. That makes it idempotent and simple, and it is the right verb when the client genuinely holds the complete current state. PATCH sends a change. It is smaller on the wire, it does not clobber fields the client never saw (new server-side fields, values edited by someone else), and it can be made conditional. The price is that both sides must agree on what the change body means, which is what the rest of this post is about.

JSON Merge Patch (RFC 7396): Send the Diff

A merge patch is a partial copy of the resource. The rules fit in three lines:

  1. Each key in the patch replaces the same key in the target.
  2. A key whose value is null is removed from the target.
  3. If both the patch value and the target value are objects, recurse. Anything else, including arrays, is replaced whole.

Given this resource:

{
  "name": "Priya Nair",
  "email": "priya@example.com",
  "nickname": "Pri",
  "address": { "city": "Sydney", "postalCode": "2000" },
  "tags": ["admin", "beta"]
}

This request:

PATCH /users/42 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "name": "Priya N.",
  "nickname": null,
  "address": { "postalCode": "2010" },
  "tags": ["admin"]
}

Produces:

{
  "name": "Priya N.",
  "email": "priya@example.com",
  "address": { "city": "Sydney", "postalCode": "2010" },
  "tags": ["admin"]
}

Note what happened: nickname is gone, address.city survived because objects merge recursively, and tags was replaced outright because arrays do not merge. If you wanted to remove only "beta", you still had to send the full array.

The readability is the selling point. The patch looks like the thing it edits, any developer can write one by hand, and a JavaScript implementation is about ten lines:

function mergePatch(target, patch) {
  if (patch === null || typeof patch !== "object" || Array.isArray(patch)) {
    return patch;                         // scalars and arrays replace
  }
  const result = (target && typeof target === "object" && !Array.isArray(target))
    ? { ...target }
    : {};
  for (const [key, value] of Object.entries(patch)) {
    if (value === null) delete result[key];
    else result[key] = mergePatch(result[key], value);
  }
  return result;
}

The two things Merge Patch cannot do

  • Set a value to null. null is spoken for; it means delete. If your schema distinguishes "no value" from "explicitly null", you need JSON Patch or a different field design. The semantics of each option are compared in null vs missing vs empty string.
  • Touch part of an array. No index addressing, no append. Every array in the patch replaces the array in the target.

JSON Patch (RFC 6902): Send the Operations

A JSON Patch body is an array of operation objects. Each has an op, a path written as a JSON Pointer (RFC 6901), and depending on the op, a value or a from. The same edit as above, plus two things Merge Patch could not express:

PATCH /users/42 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/name", "value": "Priya N." },
  { "op": "remove",  "path": "/nickname" },
  { "op": "replace", "path": "/address/postalCode", "value": "2010" },
  { "op": "remove",  "path": "/tags/1" },
  { "op": "add",     "path": "/tags/-", "value": "ops" },
  { "op": "replace", "path": "/phone", "value": null }
]

/tags/1 removes the second element by index, /tags/- appends, and the last operation stores an actual null. Pointers use / as the separator; a key that itself contains / is escaped as ~1 and ~ as ~0. The full pointer syntax, and how it differs from JSONPath, is in JSONPath vs JSON Pointer.

opMembersBehaviour
addpath, valueAdds a member, or inserts into an array at the index ("-" appends). Replaces if the member exists.
removepathDeletes the member or array element. Fails if the path does not exist.
replacepath, valueSets a new value. Fails if the path does not exist.
movefrom, pathRemoves the value at from and adds it at path.
copyfrom, pathCopies the value at from to path.
testpath, valueSucceeds only if the value at path equals value. Otherwise the whole patch fails.

Atomic by definition

RFC 6902 requires operations to be applied in order and, if any one fails, for the document to be left as it was. Removing a path that does not exist, replacing a missing key, or an index past the end of an array are all failures. Clients get a clean 409 Conflict or 422 with nothing half-applied. The error response format post shows how to report which operation failed.

The test op: optimistic concurrency for free

test asserts that a path currently holds a value. Combined with atomicity, it turns a patch into a compare-and-swap:

[
  { "op": "test",    "path": "/version", "value": 7 },
  { "op": "replace", "path": "/email",   "value": "priya.nair@example.com" },
  { "op": "replace", "path": "/version", "value": 8 }
]

If another client bumped version to 8 first, the test fails and the email change is rejected rather than silently overwriting. This solves the two-tabs problem from the introduction without ETags or extra headers.

Libraries for JSON Patch and Merge Patch

JavaScript: fast-json-patch applies, validates, and generates patches. Generating is the handy part: diff two objects and get the operations.

import { applyPatch, compare } from "fast-json-patch";

const before = { name: "Priya Nair", tags: ["admin", "beta"] };
const after  = { name: "Priya N.",   tags: ["admin"] };

const patch = compare(before, after);
// [ { op: "replace", path: "/name", value: "Priya N." },
//   { op: "remove",  path: "/tags/1" } ]

const { newDocument } = applyPatch(structuredClone(before), patch, /* validate */ true);

applyPatch mutates its first argument by default, hence the clone. For merge patches the json-merge-patch package offers apply and generate with the same idea.

Python: the jsonpatch package mirrors this API:

import jsonpatch

before = {"name": "Priya Nair", "tags": ["admin", "beta"]}
after  = {"name": "Priya N.",   "tags": ["admin"]}

patch = jsonpatch.make_patch(before, after)
print(patch.to_string())
# [{"op": "replace", "path": "/name", "value": "Priya N."},
#  {"op": "remove", "path": "/tags/1"}]

result = patch.apply(before)          # returns a new dict
# jsonpatch.JsonPatchConflict is raised if an op fails

Generated patches are also a readable changelog. If you need to review what changed between two documents rather than apply it, the approaches in how to compare two JSON files cover diffing without a patch library.

Kubernetes Uses Both

kubectl patch is the most common place developers meet these formats without knowing their names. The --type flag selects the format:

# JSON Merge Patch (RFC 7396)
kubectl patch deployment web --type merge \
  -p '{"spec": {"replicas": 3}}'

# JSON Patch (RFC 6902)
kubectl patch deployment web --type json \
  -p '[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value": "web:2.1"}]'

The default type, strategic, is a Kubernetes-specific extension of Merge Patch that knows how to merge certain arrays by a key field (such as a container's name) instead of replacing them. It exists precisely because plain Merge Patch cannot edit one container in a list. Outside Kubernetes, that job belongs to JSON Patch.

Decision Table: JSON Patch or Merge Patch?

JSON Merge PatchJSON Patch
Body shapeLooks like the resourceArray of operations
Content-Typeapplication/merge-patch+jsonapplication/json-patch+json
Delete a fieldSet it to null{ "op": "remove" }
Set a field to nullImpossible{ "op": "replace", "value": null }
Edit one array elementImpossible (whole array replaced)Yes, by index
Append to an arrayImpossiblePath ending in /-
Conditional updateNotest operation
Atomic by specNot specifiedYes, all or nothing
Readable by humansVeryModerately
Best forSimple objects, settings, profilesArrays, nested docs, concurrency control

A reasonable default for a public API: accept Merge Patch for resources that are flat objects with no meaningful arrays (settings, profiles, feature flags), and JSON Patch for anything with ordered lists or where concurrent edits are expected. Supporting both on the same endpoint is fine; the Content-Type header tells you which one arrived. Return 415 for anything else so clients find out immediately. The general shape of PATCH endpoints alongside GET, POST, and PUT is in JSON in REST APIs.

On the server, apply the patch to a copy of the stored document, validate the result against the resource schema exactly as you would a POST body, and only then write it. A patch can produce a document that is syntactically fine but semantically wrong (a required field removed, an enum set to an unknown value), and validating the patched result is far simpler than trying to validate the patch itself. Return the full updated resource in the response so the client does not have to reconstruct it.

Common Mistakes

  • Sending a merge patch with application/json. Many servers then treat it as a full replacement or reject it. Use the specific media type.
  • Expecting arrays to merge. {"tags": ["ops"]} in a merge patch does not add a tag; it leaves exactly one tag.
  • Forgetting to escape pointer keys. A key like a/b must be written /a~1b. An unescaped slash points at a child named b under a.
  • Index drift in multi-op patches. Operations run in order, so removing /tags/0 shifts every later element. Remove from the highest index down, or test first.
  • Using add when you mean replace. They behave the same on an existing object key, but add on an array index inserts rather than overwrites.
  • Hand-written patches with syntax errors. A trailing comma or a single-quoted key in a patch array gets you a 400 before the server even looks at the operations. Paste the body into JSON Prettifier to validate it, and use Sort Keys when comparing the result document to what you expected.

For a release-focused checklist after choosing the format, run these eight JSON Merge Patch API tests across serialization, authorization, candidate validation, and conditional persistence.

Frequently Asked Questions

What is the difference between JSON Patch and JSON Merge Patch?
JSON Merge Patch (RFC 7396) is a partial document: you send only the fields to change, and null deletes a field. JSON Patch (RFC 6902) is an array of explicit operations such as add, remove, and replace, each targeting a JSON Pointer path. Merge Patch is simpler; JSON Patch can edit arrays, set nulls, and run conditional updates.
Can JSON Merge Patch set a value to null?
No. In Merge Patch, null means delete this key, so there is no way to express the value null. If a field legitimately holds null, use JSON Patch with a replace operation or redesign the field to use a missing key instead.
How do I update one item in an array with a PATCH request?
Use JSON Patch with an index in the path, for example {"op": "replace", "path": "/items/2/qty", "value": 5}. JSON Merge Patch cannot address array elements; any array in a merge patch replaces the whole array on the server.
What Content-Type should a PATCH request use?
application/json-patch+json for a JSON Patch body and application/merge-patch+json for a JSON Merge Patch body. Servers use the header to decide how to interpret the body, and a plain application/json PATCH is ambiguous unless the API documents which format it expects.
Is JSON Patch atomic?
Yes. RFC 6902 requires that operations are applied in order and that if any operation fails, the whole document is left unchanged. That is what makes the test operation useful: a failed test aborts every other change in the same request.

Validate Your Patch Body First

Paste a JSON Patch array or merge patch to check the syntax before you send it. Everything runs in your browser.

Validate My JSON