Merge Patch vs. JSON Patch
HTTP PATCH is the most commonly misimplemented method in REST APIs. The reason: there are two standardized variants with very different semantics, and most implementations invent a third, proprietary variant that is neither documented nor consistent. Merge Patch and JSON Patch solve real problems, but different ones.
Table of Contents
- 1. The PATCH problem: why so many APIs get it wrong
- 2. PUT vs. PATCH: the semantic foundation
- 3. Merge Patch (RFC 7396): simple and intuitive
- 4. The null pitfall in Merge Patch
- 5. JSON Patch (RFC 6902): powerful and precise
- 6. JSON Patch operations in detail
- 7. Documenting Merge Patch in OpenAPI
- 8. Documenting JSON Patch in OpenAPI
- 9. Merge Patch vs. JSON Patch compared
- 10. Summary
- 11. FAQ
1. The PATCH problem: why so many APIs get it wrong
HTTP PATCH was defined in RFC 5789 (2010) as a method for partial modifications to a resource. What RFC 5789 explicitly does not define is how these partial changes should be specified. That is deliberate: PATCH is media-type agnostic. The concrete semantics are determined by the Content-Type. This opens the door to two standardized approaches (Merge Patch and JSON Patch) and countless proprietary variants.
The most common problem in practice: an API accepts PATCH requests with application/json as the Content-Type and a partial resource as the body, without specifying how null values are handled, whether missing fields are ignored or reset to their default, and how array fields are updated. This creates ambiguity for clients and implementation errors on the server side, because different developers on the team interpret the same PATCH logic differently.
The solution is simple: use either application/merge-patch+json (Merge Patch per RFC 7396) or application/json-patch+json (JSON Patch per RFC 6902) as the Content-Type. Both RFCs define exactly how the request body should be interpreted. This eliminates ambiguity for clients and simplifies implementation, because the server has a clear specification to follow.
2. PUT vs. PATCH: the semantic foundation
The difference between PUT and PATCH is fundamental to understanding PATCH semantics. PUT is a complete replacement: the request body contains the entire new representation of the resource. Fields missing from the body are either reset to their default value or deleted. PATCH is a partial modification: only the fields contained in the body are changed, all others remain unchanged.
PUT has the advantage of simple semantics: what you send is what you get. The downside: for large resources, clients must load the complete current version, change one field, and send the entire resource back, with the risk of overwriting changes made in the meantime (the lost-update problem). PATCH solves the lost-update problem when modeled precisely, but is semantically more complex. Both methods have their place. The rule of thumb: PUT for simple, small resources; PATCH for complex resources with many fields or frequent partial updates.
3. Merge Patch (RFC 7396): simple and intuitive
Merge Patch is the simpler of the two PATCH standards. The algorithm is intuitive: the request body is a JSON object that is merged with the resource. Fields in the body overwrite the corresponding fields in the resource. Fields that are not present in the body remain unchanged in the resource. Fields explicitly set to null in the body are removed (deleted) from the resource.
This semantic is sufficient for most use cases: updating individual string fields, toggling boolean fields, partially updating objects. The Content-Type for Merge Patch is application/merge-patch+json. When an API team explicitly uses this Content-Type, it clearly signals: "Our PATCH implementation follows RFC 7396." Clients and tooling can then handle it accordingly.
# Merge Patch, examples with request and effect
# Resource before PATCH:
# {
# "id": "abc-123",
# "name": "Summer Sale",
# "description": "Best deals of the year",
# "active": true,
# "tags": ["sale", "summer"],
# "metadata": { "createdBy": "admin", "version": 1 }
# }
# --- Example 1: Update single field ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "name": "Winter Sale" }
#
# Result:
# { "id": "abc-123", "name": "Winter Sale", "description": "Best deals...", ... }
# Only "name" is changed. All other fields are untouched.
# --- Example 2: Delete a field (set to null) ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "description": null }
#
# Result:
# { "id": "abc-123", "name": "Summer Sale", "active": true, "tags": [...] }
# "description" is REMOVED. This is the key merge-patch null semantic.
# --- Example 3: Partially update a nested object ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "metadata": { "version": 2 } }
#
# Result:
# metadata: { "createdBy": "admin", "version": 2 }
# merge-patch recurses into objects, "createdBy" is preserved.
# --- Example 4: LIMITATION, you cannot remove an array item ---
# PATCH /campaigns/abc-123
# Content-Type: application/merge-patch+json
# { "tags": ["sale"] }
#
# Result: tags is REPLACED with ["sale"], the whole array is swapped.
# Merge Patch cannot add or remove individual array items, use JSON Patch for that.
4. The null pitfall in Merge Patch
The most dangerous aspect of Merge Patch is its null semantics: null means "remove field," not "set field to null." That is counterintuitive for developers used to JSON, where null is often a valid value. The consequence: if a resource has a field that can legitimately be null (for example, endDate: null for an ongoing campaign with no end date), Merge Patch cannot distinguish between "set endDate to null" and "remove endDate from the resource", both are expressed as "endDate": null.
For APIs where fields can legitimately hold null as a value, Merge Patch is the wrong choice. JSON Patch, or a proprietary PATCH semantic with explicit operations, is better suited in that case. This pitfall should be documented explicitly in the OpenAPI documentation of the PATCH endpoint: "Null values remove the field from the resource. To set a nullable field to null, use PUT instead of PATCH."
Another pitfall: Merge Patch cannot modify arrays granularly. An array field in the body always replaces the entire array in the resource. If a client wants to add or remove one element from an array without affecting the others, it must load the entire current array, modify it, and send it back. That is the lost-update problem PATCH is actually supposed to solve, but for arrays, Merge Patch does not solve it.
5. JSON Patch (RFC 6902): powerful and precise
JSON Patch is the more powerful and precise PATCH standard. Instead of a partial resource, the client sends a list of operations to be applied to the resource. Each operation has a type (add, remove, replace, move, copy, test), a path (a JSON Pointer per RFC 6901), and, where applicable, a value.
JSON Patch solves every problem Merge Patch has: fields can be set to null (with replace), arrays can be modified granularly (with add and remove on array indices), and the test operation enables optimistic locking: if a field has the expected value before the change, the operation is applied; if not, the entire patch fails atomically. The price for this power: a more complex syntax that is overkill for simple use cases.
[
{
"comment": "Example 1: Replace a field value",
"op": "replace",
"path": "/name",
"value": "Winter Sale 2026"
},
{
"comment": "Example 2: Set a nullable field to null",
"op": "replace",
"path": "/endDate",
"value": null
},
{
"comment": "Example 3: Remove a field entirely",
"op": "remove",
"path": "/description"
},
{
"comment": "Example 4: Add an item to an array",
"op": "add",
"path": "/tags/-",
"value": "winter"
},
{
"comment": "Example 5: Remove specific array item by index",
"op": "remove",
"path": "/tags/0"
},
{
"comment": "Example 6: test operation, optimistic locking",
"op": "test",
"path": "/metadata/version",
"value": 1
},
{
"comment": "Example 7: Increment version (requires test first)",
"op": "replace",
"path": "/metadata/version",
"value": 2
},
{
"comment": "Example 8: Move a field",
"op": "move",
"from": "/oldName",
"path": "/name"
},
{
"comment": "Example 9: Copy a field",
"op": "copy",
"from": "/template/description",
"path": "/description"
}
]
6. JSON Patch operations in detail
The six JSON Patch operations cover every modification case. add: adds a value at a path. For arrays, index /- appends at the end. remove: removes the value at the given path. replace: replaces the value (equivalent to remove + add). move: moves a value from one path to another. copy: copies a value from one path to another. test: checks whether the value at the given path matches the expected value; if the check fails, the entire patch is rolled back atomically.
The test operation is the most powerful feature of JSON Patch in practice: it implements optimistic locking at the field level, without needing a separate If-Match header mechanism. If a client wants to ensure that the version field still has the value 1 before setting it to 2, it writes {"op": "test", "path": "/version", "value": 1} before the replace operation. If another client has already changed it to 2 in the meantime, the test operation fails and the entire patch request returns 409 Conflict.
7. Documenting Merge Patch in OpenAPI
Documenting Merge Patch in OpenAPI requires special care with the request body schema. Because Merge Patch makes all fields optional and null carries a special meaning (remove field), the normal resource schema cannot be reused directly. Instead, a separate "patch schema" must be defined that makes all fields optional and nullable, without those fields actually being nullable in the API itself.
The key element: the Content-Type application/merge-patch+json must be explicitly stated in the OpenAPI document as the Content-Type of the request body. This signals to tooling and consumers which standard is used. The description should explain the null semantics: "null removes the field from the resource." Many OpenAPI documents use application/json here and miss the chance to communicate the standard semantics.
# Merge Patch in OpenAPI, correct Content-Type and schema design
paths:
/campaigns/{id}:
patch:
summary: Partially update campaign (Merge Patch, RFC 7396)
description: |
Updates campaign fields using JSON Merge Patch semantics (RFC 7396).
**Merge Patch rules:**
- Fields present in the request body overwrite the existing value.
- Fields absent from the request body are left unchanged.
- Fields explicitly set to `null` are **removed** from the resource.
To set a nullable field to null, use PUT instead.
- Arrays are replaced atomically. Individual array item changes are not possible.
Use JSON Patch (PATCH with application/json-patch+json) for array item operations.
operationId: patchCampaign
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/merge-patch+json: # RFC 7396 content type
schema:
$ref: '#/components/schemas/CampaignMergePatch'
examples:
rename:
summary: Rename campaign
value: { "name": "Winter Sale 2026" }
deactivate:
summary: Deactivate campaign
value: { "active": false }
removeDescription:
summary: Remove description field
value: { "description": null }
responses:
'200':
description: Updated campaign
content:
application/json:
schema: { $ref: '#/components/schemas/Campaign' }
'400':
description: Invalid patch document
'404':
description: Campaign not found
'409':
description: Conflict, resource modified by another request
'422':
description: Patch would result in invalid resource state
components:
schemas:
CampaignMergePatch:
description: |
Merge Patch document for Campaign. All fields are optional.
Set a field to null to remove it. Absent fields are unchanged.
type: object
properties:
name:
type: string
minLength: 1
maxLength: 200
nullable: true # null = remove field
description:
type: string
maxLength: 2000
nullable: true
active:
type: boolean
nullable: true
tags:
type: array
items: { type: string }
nullable: true # null = remove field; array value = replace entire array
description: Replaces the entire tags array. Cannot add/remove individual items.
8. Documenting JSON Patch in OpenAPI
Documenting JSON Patch in OpenAPI is straightforward: the Content-Type is application/json-patch+json, and the request body is an array of operation objects. Because JSON Patch has a standardized format, the schema from the RFC can be defined directly as an OpenAPI component. The schema is not especially complex, but the combination of oneOf for the different operation types and JSON Pointer paths makes it somewhat more verbose.
A pragmatic approach: define the complete JSON Patch schema as a reusable component that can be referenced by every PATCH endpoint that uses JSON Patch. This avoids repetition and ensures that all JSON Patch endpoints use the same schema format. Examples in the OpenAPI documentation are especially important for JSON Patch, because the syntax is unfamiliar to many developers.
| Criterion | Merge Patch (RFC 7396) | JSON Patch (RFC 6902) | Recommendation |
|---|---|---|---|
| Client complexity | Low (intuitive) | Medium (operation syntax) | Merge Patch for simple cases |
| null semantics | null = remove field | null = actual null value | JSON Patch for nullable fields |
| Array modification | Full replacement only | add/remove individual items | JSON Patch for array operations |
| Optimistic locking | No (via If-Match header) | Yes (test operation) | JSON Patch for locking |
| Content-Type | application/merge-patch+json | application/json-patch+json | Always set explicitly |
# JSON Patch in OpenAPI, schema and endpoint definition
components:
schemas:
JsonPatchOperation:
type: object
required: [op, path]
properties:
op:
type: string
enum: [add, remove, replace, move, copy, test]
description: Operation type per RFC 6902
path:
type: string
description: JSON Pointer (RFC 6901) to the target location
example: /name
value:
description: Value to apply (required for add, replace, test)
from:
type: string
description: Source path (required for move, copy)
example: /oldName
JsonPatchDocument:
type: array
items:
$ref: '#/components/schemas/JsonPatchOperation'
description: Array of JSON Patch operations applied atomically (RFC 6902)
paths:
/campaigns/{id}:
patch:
summary: Patch campaign (JSON Patch, RFC 6902)
description: |
Applies a JSON Patch document to the campaign resource (RFC 6902).
Operations are applied atomically. If any operation fails, the entire
patch is rolled back and the resource is unchanged.
Use the **test** operation for optimistic locking:
```json
[
{"op": "test", "path": "/metadata/version", "value": 5},
{"op": "replace", "path": "/name", "value": "New Name"},
{"op": "replace", "path": "/metadata/version", "value": 6}
]
```
Returns 409 if the test fails (concurrent modification detected).
requestBody:
required: true
content:
application/json-patch+json: # RFC 6902 content type
schema:
$ref: '#/components/schemas/JsonPatchDocument'
examples:
addTag:
summary: Add a tag to the array
value:
- op: add
path: /tags/-
value: winter
removeTag:
summary: Remove first tag
value:
- op: remove
path: /tags/0
setNullableField:
summary: Set nullable endDate to null
value:
- op: replace
path: /endDate
value: null
responses:
'200': { description: Patched campaign }
'400': { description: Invalid patch document syntax }
'404': { description: Campaign not found }
'409': { description: test operation failed, optimistic lock conflict }
'422': { description: Patch results in invalid resource state }
10. Summary
Modeling PATCH correctly means committing to one of the two standards and consistently using it with the right Content-Type. Merge Patch (RFC 7396) is the right choice for simple partial updates where no fields are nullable and no granular array operations are needed. JSON Patch (RFC 6902) is the right choice when nullable fields, array operations, or optimistic locking are needed.
Both standards should be documented in OpenAPI with the correct Content-Type (application/merge-patch+json or application/json-patch+json respectively), not with the generic application/json. The null semantics of Merge Patch must be explained explicitly in the description field of the PATCH endpoint. JSON Patch needs good examples in the OpenAPI documentation, because the syntax is unfamiliar. Avoid proprietary PATCH implementations without a Content-Type convention, they create ambiguity for clients and implementation problems within the team.
Modeling PATCH: the essentials at a glance
Merge Patch
RFC 7396. Content-Type: application/merge-patch+json. Simple, intuitive. null = remove field (pitfall!). Arrays are replaced entirely. No optimistic locking.
JSON Patch
RFC 6902. Content-Type: application/json-patch+json. Atomic operations. null is an actual null value. Granular array operations. test operation for optimistic locking.
Decision guide
Merge Patch for simple fields without nullable values and without array operations. JSON Patch for nullable fields, array modification, or locking. Avoid proprietary PATCH semantics.
OpenAPI
State the correct Content-Type (not application/json). Keep the patch schema separate from the resource schema. Document null semantics in the description. Examples for JSON Patch are mandatory.