When updating a REST API resource, most developers default to sending the entire object with PUT. But there is a better way. PATCH requests let you update only what changed and there are two competing standards for describing those changes.
The Problem with PUT
Consider a User resource with 20 fields. When a user changes their display name, you have two options:
- PUT: Send all 20 fields, even though only 1 changed
- PATCH: Send only the 1 changed field + an update description
PATCH is clearly more efficient. But what format should the PATCH body use? That is where JSON Patch and Merge Patch diverge.
Merge Patch (RFC 7386)
Merge Patch is dead simple. Your PATCH body is a partial JSON object:
{
"displayName": "Jane Doe",
"preferences": {
"theme": "dark"
}
}
The server does a recursive merge: it replaces the specified fields, and deletes any fields set to null. That is it.
Merge Patch Rules
- Set a field: include it with a non-null value
- Delete a field: set it to
null - Replace an object: set it to a new object (not merge)
- Arrays: replaced entirely (not merged element-by-element)
Merge Patch Example
// Original
{ "name": "John", "age": 30, "address": { "city": "NYC" } }
// Merge Patch
{ "name": "Jane", "address": null }
// Result
{ "name": "Jane" }
// "age" is unchanged (stays 30)
// "address" is deleted
// "name" is replaced
When Merge Patch Works Well
- Simple form updates (display name, email, preferences)
- Resources with mostly flat structures
- When you want to delete fields by setting them to null
- Teams that find JSON Pointer syntax complex
When Merge Patch Breaks Down
- Nested objects must be fully replaced (cannot merge one nested field)
- Arrays are replaced entirely (no element-level updates)
- Cannot express "delete this field but do not touch others"
- Null has special meaning you cannot set a valid value to null
JSON Patch (RFC 6902)
JSON Patch is more powerful but more verbose. Your PATCH body is an array of operations:
[
{ "op": "replace", "path": "/displayName", "value": "Jane Doe" },
{ "op": "add", "path": "/preferences/theme", "value": "dark" },
{ "op": "remove", "path": "/legacyField" }
]
Each operation targets a location using JSON Pointer (RFC 6901), a path syntax like /users/0/email.
The Six JSON Patch Operations
addInsert or set a value at the target pathremoveDelete the value at the target pathreplaceReplace an existing value (equivalent to remove + add)moveMove a value from one path to anothercopyCopy a value to another pathtestAssert a value equals the expected value (fails the patch if not)
Array Operations
Unlike Merge Patch, JSON Patch can update specific array elements:
[
{ "op": "add", "path": "/tags/-", "value": "featured" },
{ "op": "remove", "path": "/items/2" },
{ "op": "replace", "path": "/items/0/price", "value": 29.99 }
]
The - index in JSON Pointer means "append at end" a clever touch.
The Test Operation: Atomic Updates
The test operation enables optimistic concurrency control:
[
{ "op": "test", "path": "/version", "value": 7 },
{ "op": "replace", "path": "/displayName", "value": "Jane" },
{ "op": "replace", "path": "/version", "value": 8 }
]
If another request updated the version to 9 first, the test fails and the entire patch is rejected with 409 Conflict. Critical for collaborative editing.
Head-to-Head Comparison
| Aspect | Merge Patch | JSON Patch |
|---|---|---|
| Format | Partial JSON object | Array of operations |
| Spec | RFC 7386 | RFC 6902 |
| Verbosity | Concise | Verbose |
| Nested merge | No (full replacement) | Yes |
| Array operations | Full replacement only | Add/remove/replace elements |
| Atomicity | None built-in | via test operation |
| Move/copy | No | Yes |
| Best for | Simple flat updates | Complex nested updates |
Real-World Adoption
- GitHub API: JSON Patch for issues, PRs, and repository updates
- Microsoft Graph: JSON Patch for OneDrive, Outlook, Teams updates
- Kubernetes: Strategic Merge Patch (extends JSON Patch concepts)
- Stripe API: Custom flat merge approach similar to Merge Patch
- MongoDB: Merge Patch semantics in update operations
JavaScript Implementation
// JSON Patch with fast-json-patch (3M+ weekly downloads)
import * as jsonpatch from 'fast-json-patch';
const doc = { name: "John", age: 30 };
// Generate patch from changes
const observer = jsonpatch.observe(doc);
doc.name = "Jane";
doc.age = 31;
const patch = jsonpatch.generate(observer);
// [{op:"replace",path:"/name",value:"Jane"},{op:"replace",path:"/age",value:31}]
// Apply patch
jsonpatch.applyPatch(doc, [{ op: "replace", path: "/name", value: "Alice" }]);
// Merge Patch - just use spread operator
const mergePatch = (target, patch) => ({
...target,
...Object.fromEntries(
Object.entries(patch).filter(([,v]) => v !== null)
)
});
When to Use Which
Use Merge Patch when: Your resources are mostly flat, you want to delete fields by setting them to null, simplicity is more important than precision, or you are building mobile APIs where bandwidth matters.
Use JSON Patch when: Your resources have deep nesting, you need to update specific array elements, collaborative editing is a requirement, you need atomic updates with optimistic concurrency, or move and copy operations are needed.
Key Takeaways
- Both PATCH approaches reduce bandwidth compared to full PUT
- Merge Patch is simpler but less powerful good for basic CRUD
- JSON Patch handles complex nested structures and array operations
- The
testoperation in JSON Patch is a powerful concurrency tool - Most modern APIs benefit from JSON Patch for complex resources, Merge Patch for simple ones
- You can mix approaches: Merge Patch for simple endpoints, JSON Patch for complex ones