A PUT sends the whole document, and if someone changed a field while you were looking, your copy can overwrite it. A JSON Patch (RFC 6902) sends only the difference, as data a reviewer can read line by line. Below is one patch that uses all six operations, applied in JavaScript and Python. After that come how to generate and send one, and what each library leaves behind when a patch fails halfway.
One patch can use all six operations
A patch is a JSON array of operation objects. Each has an op, a path, and sometimes a value or a from. The six ops are add, remove, replace, move, copy and test.
We will reuse this document and patch throughout:
{
"name": "red sneaker",
"sizes": [40, 41, 42],
"in_stock": true,
"tags": ["summer", "sale"]
}
[
{ "op": "test", "path": "/in_stock", "value": true },
{ "op": "add", "path": "/price", "value": 59.0 },
{ "op": "replace", "path": "/name", "value": "red sneaker, wide fit" },
{ "op": "copy", "from": "/name", "path": "/display_name" },
{ "op": "move", "from": "/tags/1", "path": "/tags/0" },
{ "op": "remove", "path": "/sizes/2" }
]
In order, it checks the stock flag, adds a price, renames the product, copies the new name into display_name, moves sale to the front of the tags and drops the last size.
Ops run in order, and every path refers to the document as the previous op left it. Two remove ops on /sizes/0 against [40, 41, 42] delete 40 and then 41, because the array shifted after the first one. When you build a patch by hand, count each index against the document as it will look at that step.
Apply it in JavaScript: the original document changes in place
npm install fast-json-patch
// apply-patch.mjs
// fast-json-patch ships CommonJS, so take the default import and destructure it
import pkg from "fast-json-patch";
const { applyPatch } = pkg;
const doc = {
name: "red sneaker",
sizes: [40, 41, 42],
in_stock: true,
tags: ["summer", "sale"],
};
const patch = [
{ op: "test", path: "/in_stock", value: true },
{ op: "add", path: "/price", value: 59.0 },
{ op: "replace", path: "/name", value: "red sneaker, wide fit" },
{ op: "copy", from: "/name", path: "/display_name" },
{ op: "move", from: "/tags/1", path: "/tags/0" },
{ op: "remove", path: "/sizes/2" },
];
const results = applyPatch(doc, patch);
// doc is already the patched document; newDocument is optional in this mode
const patched = results[results.length - 1].newDocument;
console.log(JSON.stringify(patched));
Output on Node 24:
{"name":"red sneaker, wide fit","sizes":[40,41],"in_stock":true,"tags":["sale","summer"],"price":59,"display_name":"red sneaker, wide fit"}
applyPatch returns one result per op, each carrying the newDocument as it stood after that op. The last result is the final document, and it is the same object as doc.
Apply it in Python: you get a new document back
pip install jsonpatch
# apply_patch.py
import json
import jsonpatch
doc = {
"name": "red sneaker",
"sizes": [40, 41, 42],
"in_stock": True,
"tags": ["summer", "sale"],
}
patch = jsonpatch.JsonPatch([
{"op": "test", "path": "/in_stock", "value": True},
{"op": "add", "path": "/price", "value": 59.0},
{"op": "replace", "path": "/name", "value": "red sneaker, wide fit"},
{"op": "copy", "from": "/name", "path": "/display_name"},
{"op": "move", "from": "/tags/1", "path": "/tags/0"},
{"op": "remove", "path": "/sizes/2"},
])
result = patch.apply(doc)
print(json.dumps(result))
Output on Python 3.14:
{"name": "red sneaker, wide fit", "sizes": [40, 41], "in_stock": true, "tags": ["sale", "summer"], "price": 59.0, "display_name": "red sneaker, wide fit"}
The output matches, apart from 59 in JavaScript against 59.0 in Python, which is how each language prints the same number. JsonPatch takes a parsed list, so a patch that arrives as JSON text goes through json.loads first.
Generate the patch from two documents
If you already hold a before and an after, let the library write the patch:
import pkg from "fast-json-patch";
const { compare } = pkg;
const before = { name: "red sneaker", sizes: [40, 41, 42], in_stock: true };
const after = { name: "red sneaker, wide fit", sizes: [40, 41, 42, 43], in_stock: false };
console.log(JSON.stringify(compare(before, after)));
// [{"op":"replace","path":"/in_stock","value":false},{"op":"add","path":"/sizes/3","value":43},{"op":"replace","path":"/name","value":"red sneaker, wide fit"}]
patch = jsonpatch.make_patch(before, after) # the same two dicts
print(json.dumps(patch.patch))
# [{"op": "replace", "path": "/name", "value": "red sneaker, wide fit"}, {"op": "add", "path": "/sizes/3", "value": 43}, {"op": "replace", "path": "/in_stock", "value": false}]
The libraries emit the same three ops in a different order, and applying either patch to before gives back after.
Send it as an HTTP PATCH
The body is the patch array, and the media type is application/json-patch+json:
const res = await fetch("https://api.example.com/things/42", {
method: "PATCH",
headers: { "Content-Type": "application/json-patch+json" },
body: JSON.stringify(patch),
});
A local echo server we sent this to reported method PATCH, that media type and the patch as the request body. In Python the same call is requests.patch(url, json=patch, headers={"Content-Type": "application/json-patch+json"}).
A failed patch leaves different things behind in each library
Picture an order object that is half updated in memory after a patch fails, and then gets saved anyway. Take a patch whose second op fails:
[
{ "op": "add", "path": "/b", "value": 2 },
{ "op": "test", "path": "/a", "value": 999 },
{ "op": "add", "path": "/c", "value": 3 }
]
Applied to {"a": 1}, the test fails because a is 1. Here is what each library did:
-
JavaScript, default:
applyPatchthrowsPatchError, and the document afterwards is{"a":1,"b":2}. Op 1 already ran on your object. -
JavaScript, opt-out:
applyPatch(doc, patch, false, false)still throws, and the original stays{"a":1}. -
Python, default:
patch.apply(doc)raisesJsonPatchTestFailed, and the original stays{"a": 1}. -
Python,
in_place=True: it raises the same error, and the document is left as{"a": 1, "b": 2}.
Try this: apply the three-op patch above to
{"a": 1}with the call you actually use, and print the document in thecatchorexceptbranch. Ifbis there, a failed patch half-applies in your setup.
Put test ops at the top of the patch and a stale document fails before the first change runs; they cannot catch a later op failing, so the stronger fix is to apply the patch to a copy (the default in Python, the false, false form in JavaScript) and keep the original until the whole patch has worked.
JavaScript mutates by default and Python does not, so the same failed patch can corrupt your object in one language and leave it clean in the other.
Paths are JSON Pointers, and two characters need escaping
Every path and from is a JSON Pointer (RFC 6901). Segments are separated by /, and array items are addressed by index, so /sizes/2 is the third size. On arrays, add at an index inserts before that item, and /- appends: {"op": "add", "path": "/sizes/-", "value": 43} turns [40, 41, 42] into [40, 41, 42, 43] in both libraries.
A key that contains a / has to be escaped: ~1 stands for a literal / and ~0 for a literal ~. We checked the first in Python. Adding {"op": "add", "path": "/a~1b", "value": 1} to an empty object creates the single key a/b.
A patch is data you can review, store and replay
Because a patch is the difference as data, you can review it in a pull request. An audit log that keeps one patch per edit shows who changed what and when, and replaying the patches in order on the original document rebuilds any earlier state. Go, Java and C# have libraries for the same six ops: evanphx/json-patch, zjsonpatch and JsonPatch.Net.
Sourcing note: every snippet, including the generated patches and the HTTP request against a local echo server, ran on 2026-10-28 with Node 24.18.1 and fast-json-patch 3.1.1, and Python 3.14.8 and jsonpatch 1.38.
Top comments (0)