Track 2 / Guide 12
JSON Patch and JSON Merge Patch
Apply JSON Patch and Merge Patch with correct pointer escaping, null semantics, atomic validation, authorization, and lost-update protection.
On this page
A partial update is a protocol, not a shortcut
Sending fewer fields can reduce request size, but it introduces questions a full replacement may avoid: which fields change, which remain, how deletion is represented, and what happens when another client has modified the resource. JSON Patch and JSON Merge Patch answer some of these questions differently. Select the protocol based on update semantics rather than on whichever payload looks shorter in one example.
JSON Patch is an ordered list of operations addressed by JSON Pointer paths. Merge Patch resembles the target object and describes changes through member values, with null carrying removal meaning for object members. They use different media types and should not share an ambiguous parser that guesses the format from shape.
Keep the external resource representation distinct from internal storage. A path in a patch should refer to the public contract, not arbitrary ORM properties or database internals. The REST response guide explains explicit projections and concurrency behavior. Partial update support should preserve that boundary rather than expose every writable field through a generic recursive assignment helper.
Understand operation ordering and array positions
JSON Patch operations run in sequence, so earlier changes affect later paths. Adding or removing an array element changes subsequent indices. A patch generated against one array ordering may modify the wrong record if applied after another writer reorders it. Stable object identifiers do not automatically make positional paths identity-aware.
The operations cover insertion, removal, replacement, movement, copying, and testing. Their preconditions differ: replacing an existing value is not the same as adding a member, and a removal should not be silently treated as success when the addressed location is absent. Use a maintained implementation rather than a homegrown collection of path splits and assignments.
The test operation can assert expected state inside the document, but it is not a substitute for applying the entire update atomically against the same resource version. If another transaction changes the resource between testing and writing, a non-atomic implementation still loses updates. Treat patch evaluation and persistence as one concurrency-controlled operation, with a clear failure response when preconditions do not hold.
JSON Pointer escaping is easy to get wrong
A pointer path separates reference tokens with slashes, while special characters inside a token are escaped. A property name containing a slash is not two nested properties. Likewise, a literal tilde needs its own escaping. Decode pointer tokens according to the standard in the correct order instead of applying generic URL decoding or string replacement.
Authorization must operate on the interpreted path, not an unchecked textual prefix. An allowlist that simply tests whether a string starts with a permitted word can match unintended paths. Resolve or normalize through a trusted pointer implementation, then compare the exact allowed resource fields and operations. Movement and copying also access a source path, which needs its own policy check.
Reject paths into server-owned fields such as identifiers, roles, tenant scope, or audit timestamps unless the endpoint explicitly supports them. Do not let a patch change the very ownership information used to authorize the request. The security guide explains why generic property traversal and merging require careful boundaries even when the input is valid JSON.
A runnable operation sequence
The Python example applies several operations to a synthetic document and demonstrates a failed test. Install jsonpatch, save the program as patch_demo.py, and run it locally. The library returns a new result in this usage, leaving the source fixture available for assertions. The example uses a property containing a slash to make pointer escaping visible.
The operation sequence is intentionally small enough to reason about manually. The array append uses the special insertion position, while later changes target object members. In production, cap operation count, document size, and path length before executing a patch. A small patch can still cause expensive copies if it repeatedly duplicates a large subtree.
Validate the resulting public resource after applying the patch to an isolated candidate. A patch document can be structurally valid while producing an invalid application object, such as removing a required field. The schema guide describes the contract checks that belong after transformation. Persist only after both authorization and resulting-state validation succeed under the same concurrency policy.
import jsonpatch
original = {"version": 1, "tags": ["a"], "profile": {"a/b": 2},
"status": "draft"}
operations = [
{"op": "test", "path": "/version", "value": 1},
{"op": "add", "path": "/tags/-", "value": "b"},
{"op": "replace", "path": "/profile/a~1b", "value": 3},
{"op": "copy", "from": "/status", "path": "/previousStatus"},
{"op": "move", "from": "/previousStatus", "path": "/auditStatus"},
{"op": "remove", "path": "/status"},
]
result = jsonpatch.apply_patch(original, operations, in_place=False)
assert original["profile"]["a/b"] == 2
assert result["profile"]["a/b"] == 3
assert result["tags"] == ["a", "b"]
assert result["auditStatus"] == "draft"
assert "status" not in result
try:
jsonpatch.apply_patch(original, [
{"op": "test", "path": "/version", "value": 99}
], in_place=False)
except jsonpatch.JsonPatchTestFailed:
print("stale assumption rejected")
else:
raise AssertionError("test operation unexpectedly passed")Merge Patch trades precision for a simpler shape
Merge Patch recursively updates object members. A null member removes the corresponding target member, which makes it unsuitable when an update must distinguish setting a member to null from deleting it through the same representation. Arrays are replaced as values rather than edited element by element. This is simple for many settings objects but costly or ambiguous for large ordered collections.
A non-object patch replaces the target value rather than recursively merging it. Do not implement Merge Patch as an ordinary shallow object spread and assume it follows the standard. Nested behavior, deletion, and non-object roots need explicit tests. The media type signals these semantics to the server and client.
Choose Merge Patch when clients mostly update independent object fields and its null convention matches the domain. Choose JSON Patch when explicit deletion, array edits, or tests are needed. A domain-specific command can be better than either for business transitions such as approving an invoice. In that case, expose the operation's meaning and enforce its invariants directly rather than allowing clients to patch internal state-machine fields arbitrarily.
Atomicity and conditional requests prevent lost updates
Read the resource version, evaluate the patch, validate the candidate, and write only if the version still matches. This can be implemented with a transaction or a compare-and-swap update appropriate to the database. The HTTP precondition should connect to that same version check, not merely be inspected before a later unconditional write.
When a precondition fails, return a clear response that lets the client refresh and decide how to reconcile. Do not silently rebase a positional array patch onto a changed document. A user interface may be able to reconstruct an intent-aware update, but a generic server cannot reliably infer which element the client meant after reordering.
Keep all operations in one patch atomic at the resource boundary. If operation four fails, earlier operations should not remain partially persisted. Applying operations directly to a live mutable object and saving incrementally creates surprising state. Evaluate on a candidate representation or use transactional mutation with rollback. Test failures at every operation position, not only at the first operation, to verify that partial effects cannot escape.
Authorization and audit need semantic context
Authorize the caller before exposing the current document, and authorize every requested change against the public resource policy. Some fields may be readable but not writable, and some transitions may require additional permissions. A blanket permission to update a resource does not always imply permission to change every property.
For copy and move, inspect both the source and destination. A source path could reference a confidential field and copy it into a public one. A destination could overwrite server-controlled metadata. Prefer an explicit set of supported paths for simple endpoints, and use domain commands for complex security-sensitive transitions.
Audit the resulting semantic change with appropriate redaction. Storing raw patches indefinitely can retain personal values or secrets, especially when a test operation includes an old sensitive value. Record actor, resource, version, operation category, and approved field changes according to policy. Preserve enough information for accountability without assuming a patch is safe to log because it is smaller than the full resource.
Build a compatibility and failure corpus
Include empty patches, missing paths, escaped property names, array insertion and removal, failed tests, and invalid resulting state. Test null-versus-deletion behavior separately for Merge Patch. Verify that the server rejects the wrong media type rather than interpreting a document through the wrong protocol. Use exact fixtures for pointer edge cases instead of relying only on generated examples.
Run concurrent update tests with two clients reading the same version. Confirm that one stale write fails under the chosen precondition policy and that no earlier operation leaks into storage. Add operation-count and subtree-copy limits to load tests so small malicious patches cannot create disproportionate work.
Document whether a retry is safe. A patch containing an insertion may not be idempotent when repeated after a timeout, while a conditional request can prevent duplicate application against the same version. Do not describe every PATCH request as inherently idempotent. The method, document format, operation sequence, and concurrency policy together determine retry behavior. A production implementation should make those guarantees explicit to clients.
Distinguish patch syntax validation from path authorization
Validate the operation envelope before touching the resource: allowed operation names, required members, path types, and operation-count limits. Then apply path authorization against the endpoint's public model. A syntactically valid pointer is not necessarily a permitted location, and a valid copy operation may still expose a confidential source field.
For a small settings endpoint, an explicit path allowlist is often easier to audit than a general recursive policy. Compare decoded pointer tokens rather than ambiguous textual prefixes. Include tests for property names containing slashes and tildes, and ensure an escaped spelling cannot bypass the intended comparison.
Apply resulting-state validation after the transformation. Removing a required field, changing a value to the wrong type, or violating a cross-field condition should reject the complete update. Do not partially persist the operations that happened to pass before the invalid result appeared.
Keep server-derived values outside the patchable model. Audit timestamps, resource ownership, and computed totals should usually be recalculated or managed by the server. Allowing clients to patch them directly can undermine invariants even when every individual value has the right JSON type.
Design retries around operation identity and resource version
A client that times out after submitting a patch may not know whether the server committed it. Repeating an array insertion can add another element, while repeating a removal can now target a different position or fail. Do not infer retry safety from the HTTP method name alone.
A resource-version precondition can prevent the same patch from applying again to a changed version, but the client still needs to retrieve the outcome or reconcile the current representation. For business-critical writes, an operation identifier or idempotency record may provide a clearer recovery path. Bind that identifier to the request meaning so it cannot be reused for a different update.
Test the timeout boundary after commit but before response delivery. This is where naive retry implementations often duplicate effects. Verify that the client receives a stable way to distinguish a previously completed operation from a new conflicting update. Keep the behavior documented across both JSON Patch and Merge Patch endpoints.
For collaborative editing, consider whether positional patches are the right abstraction at all. Domain commands referencing stable item identifiers can express intent more robustly than array offsets. They also make authorization and audit clearer when the operation is something like move task to column rather than replace arbitrary path.
Retain a small concurrency fixture with two clients and a shared starting version. Run it whenever persistence or middleware changes. Correct library-level patch evaluation is only one part of the guarantee; atomic storage and client recovery determine whether the full update protocol is dependable.
For Merge Patch tests, include a nested object, a replacement array, an explicit member removal, and a non-object root replacement. These fixtures expose implementations that behave like a shallow merge. Keep expected final documents in the test so reviewers can verify the semantics without reading the patch library's internals.
Engineering Comparison
| Concern | JSON Patch | Merge Patch | Design implication |
|---|---|---|---|
| Shape | Ordered operations | Target-like document | Different media types |
| Null | Can assign null explicitly | Object-member deletion | Check domain null semantics |
| Arrays | Position-based operations | Whole-array replacement | Concurrency and payload cost |
| Precondition | test operation available | No test operation | Use resource version checks |
| Deletion | remove operation | null member | Keep meaning documented |
| Atomicity | Whole operation list | Whole merge result | Validate and persist atomically |
Use application/json-patch+json for RFC 6902 and application/merge-patch+json for RFC 7396. Neither format independently solves authorization, transactions, or retry safety.