JSON Diff and Patch: RFC 6902 Operations in JavaScript
Last updated:
JSON Diff and Patch is the practice of computing the minimal set of RFC 6902 JSON Patch operations (add, remove, replace, move, copy, test) needed to transform one JSON document into another, then applying them atomically. fast-json-patch generates and applies patches with zero dependencies; json8-patch passes 100% of the RFC 6902 test suite. A diff between two 10 KB objects computes in under 1 ms with fast-json-patch.compare(). This guide covers generating diffs, applying patches atomically, using the test operation for optimistic concurrency, and the difference between JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7396). For a broader overview of the six operations, see the JSON Patch (RFC 6902) reference.
All 6 JSON Patch Operations
RFC 6902 defines exactly six operation types. Every operation object must have at least an "op" field and a "path" field. Paths use JSON Pointer notation (RFC 6901): / separates keys, /name targets the top-level name key, /address/city targets a nested key, and /tags/0 targets the first array element. The special token - appends to arrays.
// 1. add — insert or create a value
{ "op": "add", "path": "/tags/-", "value": "new" }
// 2. remove — delete a value (fails if path absent)
{ "op": "remove", "path": "/draft" }
// 3. replace — update an existing value (fails if path absent)
{ "op": "replace", "path": "/status", "value": "published" }
// 4. move — remove from "from" and insert at "path" (rename)
{ "op": "move", "from": "/title", "path": "/subject" }
// 5. copy — copy value from "from" to "path"
{ "op": "copy", "from": "/billing", "path": "/shipping" }
// 6. test — assert value equals "value"; abort patch if not
{ "op": "test", "path": "/version", "value": 5 }Operations are applied in array order. If any operation fails — a path is absent for remove or replace, an index is out of bounds, or a test assertion does not match — the entire patch is aborted and the document is returned unchanged. This all-or-nothing atomicity is one of the key advantages of JSON Patch over ad-hoc mutation. For the complete reference with deep examples for each operation, see the JSON Patch (RFC 6902) reference.
Generate a JSON Patch Diff
The fastest way to generate a JSON Patch diff in JavaScript is fast-json-patch.compare(original, updated). It performs a deep structural comparison and returns the minimal RFC 6902 patch array. Install with npm install fast-json-patch. The library has zero production dependencies and ships its own TypeScript type definitions.
import jsonpatch from 'fast-json-patch'
const original = {
name: 'Alice',
age: 30,
roles: ['user'],
address: { city: 'Austin', zip: '78701' },
}
const updated = {
name: 'Bob',
roles: ['user', 'admin'],
address: { city: 'Austin', zip: '78702' },
}
const patch = jsonpatch.compare(original, updated)
// [
// { op: 'replace', path: '/name', value: 'Bob' },
// { op: 'remove', path: '/age' },
// { op: 'add', path: '/roles/1', value: 'admin' },
// { op: 'replace', path: '/address/zip', value: '78702' },
// ]For a purely visual diff without generating a patch array, Jsonic's JSON Diff tool shows added, removed, and changed values highlighted side-by-side in the browser. The json-diff-patch npm package is another alternative with a more expressive delta format suited to nested array diffing, though its output is not RFC 6902-compatible. For a comparison of diff approaches in JavaScript, see the JSON diff in JavaScript guide.
Apply a JSON Patch
fast-json-patch.applyPatch(document, patch) returns an object with a newDocument property — the patched result — without mutating the original. Each element of the result array corresponds to the return value of the individual operation (useful for debugging). If any operation throws, applyPatch stops and re-throws; the original document is unmodified.
import jsonpatch from 'fast-json-patch'
const doc = { name: 'Alice', version: 1, tags: ['sale'] }
const patch = [
{ op: 'replace', path: '/name', value: 'Bob' },
{ op: 'add', path: '/tags/-', value: 'new' },
{ op: 'replace', path: '/version', value: 2 },
]
// Non-mutating (default)
const { newDocument } = jsonpatch.applyPatch(doc, patch)
// newDocument = { name: 'Bob', version: 2, tags: ['sale', 'new'] }
// doc is unchanged
// Mutating (faster for large documents — pass true as 3rd arg)
jsonpatch.applyPatch(doc, patch, true)
// doc is now mutated in placeWhen the original document must be preserved (for undo support or audit logging), deep-clone before applying a mutating patch: const clone = JSON.parse(JSON.stringify(doc)). For documents containing Date objects or non-JSON types, use a structured clone via structuredClone(doc) (Node 17+, all modern browsers). The validate(patch) utility checks patch syntax before application — useful during development to catch malformed operations early.
The test Operation and Optimistic Concurrency
The test operation asserts that the value at a given path equals an expected value. If the assertion fails, the entire patch is aborted — no subsequent operations run. This fail-fast semantic makes test the foundation of optimistic concurrency control: instead of locking a resource while editing, a client reads the document, records its version, makes edits, and sends a patch that first asserts the version has not changed.
// Client reads document at version 5
// Client sends HTTP PATCH /documents/42
// Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/version", "value": 5 },
{ "op": "replace", "path": "/status", "value": "published" },
{ "op": "replace", "path": "/version", "value": 6 }
]
// Server applies the patch atomically.
// If another writer already incremented version to 6,
// the test fails → 409 Conflict, client must re-fetch and retry.Pair this with the HTTP ETag / If-Match pattern for full HTTP-level optimistic concurrency: the server sets ETag: "v5" on reads; the client sends If-Match: "v5" on PATCH requests; the server rejects with 412 Precondition Failed if the ETag no longer matches. The JSON Patch test operation provides the same guarantee at the document payload level, independent of HTTP headers — useful for WebSocket or non-HTTP transports.
Patching JSON Arrays
JSON Patch uses integer index paths for arrays. Index /tags/0 targets the first element; /tags/- (append sentinel) is valid only for add. After a remove, remaining elements shift down — removing index 1 makes the former index 2 become the new index 1. When removing multiple elements in a single patch, remove from the highest index first to avoid off-by-one errors.
// Original: { tags: ['sale', 'draft', 'new'] }
// Append
{ "op": "add", "path": "/tags/-", "value": "featured" }
// → ['sale', 'draft', 'new', 'featured']
// Insert at index 0
{ "op": "add", "path": "/tags/0", "value": "promo" }
// → ['promo', 'sale', 'draft', 'new', 'featured']
// Remove by index (remove highest first when batching)
{ "op": "remove", "path": "/tags/2" } // removes 'draft'
// Move (reorder): bring index 3 to front
{ "op": "move", "from": "/tags/3", "path": "/tags/0" }
// Replace a specific element
{ "op": "replace","path": "/tags/1", "value": "clearance" }Generating diffs for arrays is the hardest part of JSON Patch diffing. fast-json-patch.compare() compares arrays by position — if you insert an element at index 0, every subsequent element appears as a replace rather than an add followed by unchanged elements. This is the classic O(n²) diff problem. For order-sensitive arrays where minimizing patch size matters, consider sorting the arrays by a stable key before diffing, or use a longest-common-subsequence algorithm. For more on this challenge, see the JSON diff tutorial.
JSON Patch vs JSON Merge Patch
RFC 6902 (JSON Patch) and RFC 7396 (JSON Merge Patch) are both IETF standards for describing partial updates to a JSON document, but they differ significantly in expressiveness and simplicity. JSON Merge Patch is a partial JSON document: present keys overwrite, null values delete, absent keys are left unchanged. It is intuitive for simple object field updates but fundamentally cannot express array element insertion or removal without replacing the entire array.
| JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) | |
|---|---|---|
| Format | Array of operation objects | Partial JSON document |
| Delete a key | remove operation | Set key to null |
| Set key to null | replace with null | Not possible (null means delete) |
| Rename a key | Yes (move) | No |
| Array insert by index | Yes | No (replaces entire array) |
| Conditional update | Yes (test) | No |
| Content-Type | application/json-patch+json | application/merge-patch+json |
Choose JSON Merge Patch when your update semantics naturally map to "set these fields, delete these fields" and arrays are always replaced wholesale. Choose JSON Patch when you need to append to an array, remove a specific element by index, rename a key, or apply conditional updates with test. For a deep dive, see the JSON Merge Patch (RFC 7396) guide.
HTTP PATCH with JSON Patch
The HTTP PATCH method applies partial modifications to a resource. When using JSON Patch, set the request's Content-Type header to application/json-patch+json. The request body is the RFC 6902 patch array. Servers should return 200 OK with the updated document, 204 No Content if returning no body, 409 Conflict if a test assertion failed, or 422 Unprocessable Entity if the patch is malformed.
// Client — fetch
const patch = [
{ op: 'test', path: '/version', value: 3 },
{ op: 'replace', path: '/status', value: 'published' },
{ op: 'replace', path: '/version', value: 4 },
]
const res = await fetch('/api/articles/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json-patch+json' },
body: JSON.stringify(patch),
})
if (res.status === 409) {
// test failed — re-fetch and retry
}
// Express.js server
import jsonpatch from 'fast-json-patch'
app.patch('/api/articles/:id', express.json({ type: 'application/json-patch+json' }), async (req, res) => {
const doc = await db.get(req.params.id)
try {
const { newDocument } = jsonpatch.applyPatch(doc, req.body)
await db.save(req.params.id, newDocument)
res.json(newDocument)
} catch (err) {
res.status(409).json({ error: err.message })
}
})In FastAPI (Python), install pip install jsonpatch and use jsonpatch.apply_patch(doc, patch_list). Wrap in a try/except to catch jsonpatch.JsonPatchTestFailed and return a 409. For comparing two JSON documents before building a patch, use Jsonic's JSON Diff tool. For verifying JSON structure before patching, use Jsonic's JSON validator.
Key Terms
- JSON Patch (RFC 6902)
- An IETF standard (April 2013) for describing a sequence of changes to a JSON document as an array of operation objects. Operations are applied atomically. Used with HTTP PATCH and
Content-Type: application/json-patch+json. - JSON Pointer (RFC 6901)
- The path syntax used in JSON Patch
"path"and"from"fields. Tokens are separated by/;~0escapes a literal~;~1escapes a literal/in a key name. The empty string""refers to the entire document. - Operation
- A single change in a JSON Patch document. One of:
add,remove,replace,move,copy, ortest. Each operation is a JSON object with at least an"op"and a"path"field. - Atomic application
- All operations in a JSON Patch are applied as a unit: either every operation succeeds and the document is updated, or a failure at any step aborts the entire patch and the document is left unchanged. No partial application is possible.
- Optimistic concurrency
- A concurrency control strategy where a client reads a resource, makes changes, and submits them with a precondition asserting the resource has not changed since it was read. If the precondition fails, the update is rejected. The JSON Patch
testoperation implements this at the document level. - JSON Merge Patch (RFC 7396)
- A simpler alternative to JSON Patch where the patch is a partial JSON document. Keys in the patch overwrite the target;
nullvalues delete keys; absent keys are left unchanged. Cannot express move, copy, test, or precise array element operations. UsesContent-Type: application/merge-patch+json. - fast-json-patch
- The most widely used JavaScript library for generating and applying RFC 6902 JSON Patches. Zero production dependencies, ships TypeScript types, 2M+ weekly npm downloads. Key functions:
compare(original, updated),applyPatch(doc, patch),validate(patch).
Frequently Asked Questions
How do I generate a JSON Patch diff in JavaScript?
Use fast-json-patch: npm install fast-json-patch, then call jsonpatch.compare(original, updated). The function returns an RFC 6902 patch array — the minimal set of operations to transform the original into the updated document. It performs a deep structural diff and generates correct JSON Pointer paths for nested keys and array elements. For a purely visual diff, use Jsonic's JSON Diff tool. For other JavaScript diff approaches, see the JSON diff in JavaScript guide.
How do I apply a JSON Patch to an object?
With fast-json-patch: const { newDocument } = jsonpatch.applyPatch(doc, patch). The function does not mutate the original document — it returns an object with a newDocument property. Operations are applied atomically: a failure on any operation aborts the entire patch. Pass true as the third argument for in-place mutation (faster for large documents). Always deep-clone the document before applying a mutating patch if you need the original preserved.
What is the difference between JSON Patch and JSON Merge Patch?
JSON Patch (RFC 6902) is an array of operation objects with six possible operations including move, copy, and test. JSON Merge Patch (RFC 7396) is a partial JSON document — simpler to read and write, but limited: it cannot express array element removal without replacing the entire array, cannot rename keys, and cannot set a value to null (null means delete). See the full comparison in the JSON Merge Patch (RFC 7396) guide.
How do I use the JSON Patch test operation?
Prepend {"op": "test", "path": "/fieldName", "value": expectedValue} to your patch array. If the value at that path does not match expectedValue, the entire patch aborts. This is the core of optimistic concurrency: the client reads a document at version N, builds a patch that first asserts version === N, then makes its changes and increments the version. If another writer changed the document, the test fails and the client receives a conflict response.
Can JSON Patch add or remove array elements?
Yes. To append: {"op": "add", "path": "/tags/-", "value": "new"}. To insert at index 0: {"op": "add", "path": "/tags/0", "value": "featured"}. To remove at index 2: {"op": "remove", "path": "/tags/2"}. When removing multiple elements in one patch, remove from the highest index first to avoid index shifting errors.
What is the Content-Type for a JSON Patch request?
application/json-patch+json. Set this as the Content-Type header in HTTP PATCH requests. The body is a JSON array of RFC 6902 operation objects. JSON Merge Patch uses application/merge-patch+json with a plain JSON object body. Servers use the Content-Type to select the correct patch algorithm.
Is JSON Patch applied atomically?
Yes, per RFC 6902. All operations succeed together or none are applied — the document is never left in a partially-patched state. This guarantee makes JSON Patch safe for concurrent update scenarios, especially when combined with the test operation as a precondition guard.
How do I implement optimistic locking with JSON Patch?
Store a version integer in the document. When a client reads the document it receives the current version. Its PATCH request begins with {"op": "test", "path": "/version", "value": N}. Subsequent operations apply the intended changes and increment the version. If another writer already incremented the version, the test fails and the server returns 409 Conflict. The client re-fetches the latest document and retries. Pair with HTTP ETag + If-Match headers for full RFC-compliant conditional request support.
Further reading and primary sources
- RFC 6902 — JavaScript Object Notation (JSON) Patch — The authoritative IETF specification for the JSON Patch format.
- RFC 6901 — JavaScript Object Notation (JSON) Pointer — The path syntax used in JSON Patch "path" and "from" fields.
- RFC 7396 — JSON Merge Patch — The simpler alternative to JSON Patch for partial document updates.
- fast-json-patch on npm — The most popular JavaScript library for applying and generating RFC 6902 patches.
- json8-patch — 100% RFC 6902 test suite compliance — Alternative library that passes every test in the official RFC 6902 test suite.
Generate a JSON Patch diff from two documents instantly in your browser.
Open JSON Diff