Dev Tools

JSON Patch (RFC 6902): The Complete Guide to Surgical JSON Document Updates

What Is JSON Patch (RFC 6902)?

JSON Patch is an IETF standard defined in RFC 6902 that describes a format for expressing a sequence of operations to apply to a JSON document. Rather than sending an entire updated document over the network, a client can send a compact patch that describes only the changes. The server applies those changes atomically and returns the modified document or a confirmation. JSON Patch uses the media type application/json-patch+json.

A JSON Patch document is a JSON array of operation objects. Each object contains at minimum an op field (the operation name) and a path field (a JSON Pointer string that identifies the target location within the document). Some operations require additional fields such as value or from. When a patch is applied, operations execute sequentially from first to last. If any operation fails, the entire patch is rolled back and the target document remains unchanged.

This transactional, all-or-nothing behavior makes JSON Patch fundamentally different from ad-hoc field updates. It guarantees consistency: either every operation succeeds or the document is untouched. This property is critical in concurrent systems where multiple clients may be modifying the same resource simultaneously. Use our JSON diff tool to generate JSON Patch documents from any two JSON objects directly in your browser.

JSON Pointer (RFC 6901): Addressing Values Inside a JSON Document

Before understanding JSON Patch operations, you need to understand JSON Pointer, defined in RFC 6901. A JSON Pointer is a string that identifies a specific value within a JSON document. It consists of a sequence of reference tokens separated by forward slashes (/). The empty string "" points to the entire document root.

Consider this JSON document:

{
"users": [
  { "name": "Alice", "roles": ["admin", "editor"] },
  { "name": "Bob", "roles": ["viewer"] }
],
"settings": {
  "theme": "dark",
  "notifications": { "email": true, "sms": false }
}
}

The following JSON Pointers resolve to specific values within that document:

  • /users — the entire users array
  • /users/0 — the first user object ({"name": "Alice", ...})
  • /users/0/name — the string "Alice"
  • /users/1/roles/0 — the string "viewer"
  • /settings/notifications/email — the boolean true

Two characters have special meaning in JSON Pointer and must be escaped: the tilde (~) is escaped as ~0, and the forward slash (/) is escaped as ~1. For example, a key named a/b is referenced as /a~1b, and a key named m~n is referenced as /m~0n. Array elements are referenced by their zero-based index. The special token - refers to the position past the last element of an array and is used with the add operation to append values.

The Six JSON Patch Operations

RFC 6902 defines exactly six operations. Each serves a distinct purpose, and together they can express any transformation from one JSON document to another.

1. add — Insert or Set a Value

The add operation inserts a value at the target location. If the target is an object member that already exists, the existing value is replaced. If the target is an array index, the new value is inserted before the element at that index, shifting subsequent elements. If the target uses the - token on an array, the value is appended to the end.

{ "op": "add", "path": "/settings/language", "value": "en-US" }
{ "op": "add", "path": "/users/1", "value": { "name": "Charlie" } }
{ "op": "add", "path": "/users/-", "value": { "name": "Dana" } }

The first example adds a new language field to settings. The second inserts a new user at index 1, shifting Bob to index 2. The third appends a new user at the end of the array. The add operation requires the parent of the target to exist; you cannot add /a/b/c if /a/b does not exist.

2. remove — Delete a Value

The remove operation deletes the value at the specified path. When removing an array element, subsequent elements shift down to fill the gap. The operation fails if the target path does not exist in the document.

{ "op": "remove", "path": "/settings/notifications/sms" }
{ "op": "remove", "path": "/users/0/roles/1" }

The first example removes the sms field from notifications. The second removes the element at index 1 from Alice's roles array (which would remove "editor"). Always verify that the path exists before issuing a remove — or pair it with a test operation for safety.

3. replace — Overwrite an Existing Value

The replace operation overwrites the value at the target path with a new value. It is semantically identical to a remove followed by an add at the same path, but expressed as a single operation. The target path must already exist; replacing a nonexistent path is an error.

{ "op": "replace", "path": "/users/0/name", "value": "Alicia" }
{ "op": "replace", "path": "/settings/theme", "value": "light" }

4. move — Relocate a Value

The move operation removes the value at the from path and adds it at the path location. This is useful for renaming fields or reordering array elements without duplicating data. The from path must not be a proper prefix of path — you cannot move a value into its own child.

{ "op": "move", "from": "/users/1", "path": "/users/0" }

This example moves the second user to the first position in the array, effectively reordering the users.

5. copy — Duplicate a Value

The copy operation reads the value at the from path and adds it at the path location. The source value remains unchanged. The copied value is a deep copy, so modifications to the copy do not affect the original.

{ "op": "copy", "from": "/settings/theme", "path": "/users/0/preferredTheme" }

This copies the current theme setting into the first user's profile as a preferredTheme field.

6. test — Assert a Value Before Patching

The test operation checks that the value at the specified path is equal to the provided value. Equality is deep: objects and arrays are compared recursively, and order matters for arrays. If the test fails, the entire patch is aborted and no operations are applied — including operations that appeared before the test in the array.

[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/version", "value": 4 },
{ "op": "replace", "path": "/data", "value": "updated content" }
]

This patch first verifies that the document's version is 3. Only if that assertion passes will the version be incremented to 4 and the data replaced. This pattern implements optimistic concurrency control: the client reads the current version, builds a patch that asserts the version it read, and submits the patch. If another client modified the document in the meantime, the test fails and the patch is rejected, preventing lost updates.

Using JSON Patch with REST APIs

The HTTP PATCH method, defined in RFC 5789, is the natural vehicle for JSON Patch. Unlike PUT, which requires sending the full resource representation, PATCH sends only the changes. The client sets the Content-Type header to application/json-patch+json to signal that the request body is a JSON Patch document.

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

[
{ "op": "replace", "path": "/email", "value": "[email protected]" },
{ "op": "add", "path": "/phone", "value": "+1-555-0123" },
{ "op": "remove", "path": "/legacyId" }
]

The server reads the current state of user 42, applies the three operations in order, validates the result, persists it, and returns the updated resource (typically with a 200 OK status). If any operation fails — for example, if /legacyId does not exist — the server returns a 422 Unprocessable Entity or 409 Conflict status and the resource remains unchanged.

Content Negotiation

Servers that support both JSON Patch and JSON Merge Patch use the Content-Type header to distinguish between them. A request with application/json-patch+json is interpreted as RFC 6902 JSON Patch. A request with application/merge-patch+json is interpreted as RFC 7396 JSON Merge Patch. Servers should document which patch formats they accept and return 415 Unsupported Media Type for unrecognized content types.

Applying JSON Patch in JavaScript, Python, and Go

Every major programming language has mature libraries for applying JSON Patch documents. Here are practical examples in three popular languages.

JavaScript (Node.js and Browser)

The fast-json-patch library is the most widely used JavaScript implementation, conforming fully to RFC 6902.

import { applyPatch, validate } from 'fast-json-patch';

const document = { name: "Alice", age: 30, roles: ["admin"] };
const patch = [
{ op: "replace", path: "/name", value: "Alicia" },
{ op: "add", path: "/roles/-", value: "editor" },
{ op: "remove", path: "/age" }
];

const errors = validate(patch, document);
if (errors) {
console.error("Invalid patch:", errors);
} else {
const result = applyPatch(document, patch);
console.log(result.newDocument);
// { name: "Alicia", roles: ["admin", "editor"] }
}

The validate function checks the patch against the document before applying it, catching errors like nonexistent paths or invalid operations without mutating the original.

Python

The jsonpatch library provides a clean Pythonic interface to RFC 6902.

import jsonpatch

document = {"name": "Alice", "age": 30, "roles": ["admin"]}
patch = jsonpatch.JsonPatch([
  {"op": "replace", "path": "/name", "value": "Alicia"},
  {"op": "add", "path": "/roles/-", "value": "editor"},
  {"op": "remove", "path": "/age"}
])

try:
  result = patch.apply(document)
  print(result)
  # {'name': 'Alicia', 'roles': ['admin', 'editor']}
except jsonpatch.JsonPatchException as e:
  print(f"Patch failed: {e}")

Python's jsonpatch also supports generating patches from two documents with jsonpatch.make_patch(source, target), which returns a JsonPatch object that can be serialized and transmitted.

Go

The jsonpatch package by Matt Baird provides JSON Patch support in Go.

import (
  "fmt"
  jsonpatch "github.com/evanphx/json-patch/v5"
)

original := []byte(`{"name":"Alice","age":30,"roles":["admin"]}`)
patchJSON := []byte(`[
  {"op":"replace","path":"/name","value":"Alicia"},
  {"op":"add","path":"/roles/-","value":"editor"},
  {"op":"remove","path":"/age"}
]`)

patch, err := jsonpatch.DecodePatch(patchJSON)
if err != nil {
  log.Fatalf("Invalid patch: %v", err)
}
result, err := patch.Apply(original)
if err != nil {
  log.Fatalf("Apply failed: %v", err)
}
fmt.Println(string(result))
// {"name":"Alicia","roles":["admin","editor"]}

Error Handling and Atomicity

RFC 6902 mandates that patch application is atomic. If operation number 5 in a 10-operation patch fails, operations 1 through 4 must be rolled back. The document must remain exactly as it was before the patch was attempted. This requirement has important implementation implications.

Common Error Conditions

  • Path does not exist: A remove, replace, or move operation targets a path that is not present in the document. This is the most common error and usually indicates a stale client that is working with an outdated view of the resource.
  • Test assertion failure: A test operation finds that the value at the specified path does not match the expected value. This signals a concurrent modification and the patch should be retried after fetching the latest state.
  • Type mismatch: An operation expects the target to be an object but finds an array, or vice versa. For example, using an array index on an object path like /settings/0 when settings is an object.
  • Array index out of bounds: An add operation specifies an array index greater than the array length, or a remove targets an index beyond the last element.
  • Invalid JSON Pointer: The path string does not conform to RFC 6901 syntax — missing leading slash, unescaped tilde, or non-numeric array index token.

Retry Strategies

When a patch fails due to a concurrent modification (detected via a test operation), the standard retry pattern is:

  • Fetch the latest version of the resource.
  • Recompute the patch based on the new state and the desired changes.
  • Submit the new patch with an updated test operation reflecting the current version.
  • Repeat up to a maximum retry count, then fail with a conflict error.

This pattern is a form of optimistic concurrency control and avoids the need for pessimistic locks on the server.

Security Considerations

JSON Patch introduces security concerns that developers must address in production systems.

Path Traversal and Injection

Because JSON Pointer paths are strings, a malicious client could craft paths that target sensitive fields the client should not be able to modify. For example, a user submitting a patch to update their profile should not be able to include {"op": "replace", "path": "/role", "value": "admin"}. Servers must validate that every path in a patch targets an allowed field. Implement a whitelist of patchable paths per resource type and reject patches that reference unauthorized fields.

Denial of Service

An attacker could submit a patch with thousands of operations, extremely deep paths, or very large values to exhaust server resources. Defend against this by limiting the maximum number of operations per patch, the maximum depth of JSON Pointer paths, and the maximum size of the patch document. Return 413 Payload Too Large or 422 Unprocessable Entity for patches that exceed these limits.

Data Leakage via test Operations

The test operation can be used as an oracle to probe the contents of a document. An attacker submits patches with test operations targeting different values and observes whether the patch succeeds or fails. If the server returns different error codes or messages for test failures versus path-not-found errors, the attacker can infer field existence and values. Mitigate this by returning generic error responses that do not distinguish between failure modes, and by rate-limiting PATCH requests.

JSON Merge Patch (RFC 7396) vs. JSON Patch (RFC 6902)

RFC 7396 JSON Merge Patch is an alternative approach to partial updates that is simpler but less powerful. Understanding when to use each format is essential for API design.

How JSON Merge Patch Works

A Merge Patch document is a regular JSON object. The server recursively merges it into the target document following these rules:

  • If a field in the patch has a non-null value, it replaces the corresponding field in the target (or creates it if absent).
  • If a field in the patch has a null value, the corresponding field is removed from the target.
  • Fields in the target that are not mentioned in the patch remain unchanged.
// Target document
{ "name": "Alice", "age": 30, "nickname": "Ali" }

// Merge Patch
{ "age": 31, "nickname": null, "email": "[email protected]" }

// Result
{ "name": "Alice", "age": 31, "email": "[email protected]" }

When to Choose Which Format

Use JSON Merge Patch when your updates are simple field-level changes on flat or shallow objects, when you do not need to manipulate individual array elements, and when null is not a meaningful value in your domain (since it is used as the deletion signal).

Use JSON Patch (RFC 6902) when you need to manipulate specific array elements by index, when you need move or copy operations, when you need test operations for optimistic concurrency, when null is a valid value that should be distinguishable from field deletion, or when you need an auditable log of individual mutations.

Key Limitations of JSON Merge Patch

  • No array element operations: Merge Patch always replaces the entire array. You cannot add, remove, or modify individual elements. For a document with a tags array of 100 elements, adding one tag requires sending all 101 tags.
  • Null ambiguity: Setting a field to null and deleting a field are the same operation. If your data model uses null meaningfully (e.g., "user has not set a value" versus "user cleared the value"), Merge Patch cannot express this distinction.
  • No precondition checks: There is no equivalent to the test operation. You cannot conditionally apply a Merge Patch based on the current state of the document.
  • No operation history: A Merge Patch is a snapshot of desired field values, not a sequence of discrete operations. You lose the ability to see exactly what changed step by step.

Practical Patterns and Real-World Usage

Optimistic Concurrency with test

The most powerful pattern in JSON Patch is combining test with mutation operations to implement optimistic locking without server-side lock infrastructure. The client reads a resource, notes a version field (or any field that changes on each update), and submits a patch that tests the version before making changes. If another client updated the resource between the read and the patch, the test fails and the patch is rejected.

[
{ "op": "test", "path": "/updatedAt", "value": "2026-03-17T10:00:00Z" },
{ "op": "replace", "path": "/status", "value": "approved" },
{ "op": "replace", "path": "/updatedAt", "value": "2026-03-18T14:30:00Z" }
]

Batch Field Renames During Migrations

When evolving an API schema, you may need to rename fields across all stored documents. JSON Patch's move operation handles this cleanly:

[
{ "op": "move", "from": "/userName", "path": "/username" },
{ "op": "move", "from": "/emailAddress", "path": "/email" }
]

This is safer than separate remove-and-add operations because move is atomic: if the target path already exists, the operation replaces it with the moved value, and the source is always removed.

Generating Patches from Diffs

Most JSON Patch libraries include a diff function that takes two documents and produces the minimal patch needed to transform one into the other. This is invaluable for building audit logs, synchronization protocols, and undo/redo systems. Generate a diff between any two JSON documents using our JSON diff tool, which runs entirely in your browser with no data transmitted to external servers.

Conclusion

JSON Patch (RFC 6902) is a precise, standardized protocol for expressing changes to JSON documents. Its six operations — add, remove, replace, move, copy, and test — cover every mutation scenario from simple field updates to complex conditional transformations with optimistic concurrency control. Combined with JSON Pointer (RFC 6901) for addressing, it provides a complete, interoperable system for partial document updates across HTTP APIs, real-time collaboration systems, and data synchronization pipelines. For simpler use cases, JSON Merge Patch (RFC 7396) offers a more compact alternative at the cost of expressiveness. Choose the right tool for your use case, validate patches on the server, guard against security risks, and leverage the rich library ecosystem in JavaScript, Python, Go, and other languages to integrate JSON Patch into your applications.

← Back to Blog