Merge Patch vs. JSON Patch
HTTP PATCH ist die am häufigsten falsch implementierte Methode in REST APIs. Der Grund: Es gibt zwei standardisierte Varianten mit sehr unterschiedlicher Semantik, und die meisten Implementierungen erfinden eine dritte, proprietäre Variante, die weder dokumentiert noch konsistent ist. Merge Patch und JSON Patch lösen echte Probleme – aber unterschiedliche.
Inhaltsverzeichnis
- 1. Das PATCH-Problem: warum so viele APIs es falsch machen
- 2. PUT vs. PATCH: die semantische Grundlage
- 3. Merge Patch (RFC 7396): einfach und intuitiv
- 4. Der null-Fallstrick bei Merge Patch
- 5. JSON Patch (RFC 6902): mächtig und präzise
- 6. JSON Patch Operationen im Detail
- 7. Merge Patch in OpenAPI dokumentieren
- 8. JSON Patch in OpenAPI dokumentieren
- 9. Merge Patch vs. JSON Patch im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das PATCH-Problem: warum so viele APIs es falsch machen
HTTP PATCH wurde im RFC 5789 (2010) definiert als Methode für partial modifications an einer Ressource. Was RFC 5789 explizit nicht definiert: wie diese partiellen Änderungen spezifiziert werden sollen. Das ist bewusst: PATCH ist medienstyp-agnostisch. Die konkrete Semantik wird durch den Content-Type bestimmt. Das öffnet die Tür für zwei standardisierte Ansätze (Merge Patch und JSON Patch) und unzählige proprietäre Varianten.
Das häufigste Problem in der Praxis: Eine API akzeptiert PATCH-Requests mit application/json als Content-Type und einer partiellen Ressource als Body – ohne zu spezifizieren, wie null-Werte behandelt werden, ob fehlende Felder ignoriert oder auf den Default zurückgesetzt werden, und wie Array-Felder aktualisiert werden. Das erzeugt Ambiguität für Clients und Implementierungsfehler auf Serverseite, weil verschiedene Entwickler im Team dieselbe PATCH-Logik unterschiedlich interpretieren.
Die Lösung ist einfach: Entweder application/merge-patch+json (Merge Patch nach RFC 7396) oder application/json-patch+json (JSON Patch nach RFC 6902) als Content-Type verwenden. Beide RFCs definieren exakt, wie der Request-Body interpretiert werden soll. Das eliminiert Ambiguität für Clients und vereinfacht die Implementierung, weil der Server eine klare Spezifikation hat.
2. PUT vs. PATCH: die semantische Grundlage
Der Unterschied zwischen PUT und PATCH ist fundamental für das Verständnis von PATCH-Semantik. PUT ist eine vollständige Ersetzung: Der Request-Body enthält die komplette neue Repräsentation der Ressource. Felder, die im Body fehlen, werden entweder auf ihren Default-Wert zurückgesetzt oder gelöscht. PATCH ist eine partielle Modifikation: Nur die im Body enthaltenen Felder werden geändert, alle anderen bleiben unverändert.
PUT hat den Vorteil der einfachen Semantik: Was du sendest, ist was du kriegst. Der Nachteil: Bei großen Ressourcen müssen Clients die komplette aktuelle Version laden, ein Feld ändern und die ganze Ressource zurücksenden – mit dem Risiko, in der Zwischenzeit gemachte Änderungen zu überschreiben (Lost-Update-Problem). PATCH löst das Lost-Update-Problem bei präziser Modellierung, ist aber semantisch komplexer. Beide Methoden haben ihren Platz – die Faustregel: PUT für einfache, kleine Ressourcen; PATCH für komplexe Ressourcen mit vielen Feldern oder häufigen partiellen Updates.
3. Merge Patch (RFC 7396): einfach und intuitiv
Merge Patch ist der einfachere der beiden PATCH-Standards. Der Algorithmus ist intuitiv: Der Request-Body ist ein JSON-Objekt, das mit der Ressource gemergt wird. Felder im Body überschreiben die entsprechenden Felder in der Ressource. Felder, die im Body nicht vorhanden sind, bleiben in der Ressource unverändert. Felder, die im Body explizit auf null gesetzt sind, werden aus der Ressource entfernt (gelöscht).
Diese Semantik ist für die meisten Use Cases ausreichend: Einzelne String-Felder aktualisieren, Boolean-Felder umschalten, Objekte partiell aktualisieren. Der Content-Type für Merge Patch ist application/merge-patch+json. Wenn ein API-Team diesen Content-Type explizit verwendet, signalisiert es damit klar: "Unsere PATCH-Implementierung folgt RFC 7396." Clients und Tooling können dann entsprechend damit umgehen.
# 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. Der null-Fallstrick bei Merge Patch
Der gefährlichste Aspekt von Merge Patch ist die null-Semantik: null bedeutet "Feld entfernen", nicht "Feld auf null setzen". Das ist kontraintuitiv für Entwickler, die JSON kennen, wo null oft ein gültiger Wert ist. Die Konsequenz: Wenn eine Ressource ein Feld hat, das tatsächlich null sein kann (z. B. endDate: null bei einer unbefristeten Kampagne), dann kann Merge Patch nicht zwischen "setze endDate auf null" und "entferne endDate aus der Ressource" unterscheiden – beides ist "endDate": null.
Für APIs, bei denen Felder legitim null als Wert haben können, ist Merge Patch die falsche Wahl. JSON Patch oder eine proprietäre PATCH-Semantik mit expliziten Operationen sind dann besser geeignet. Dieser Fallstrick sollte in der OpenAPI-Dokumentation des PATCH-Endpoints explizit dokumentiert werden: "Null-Werte entfernen das Feld aus der Ressource. Um ein Feld auf null zu setzen, das nullable ist, verwende PUT statt PATCH."
Ein weiterer Fallstrick: Merge Patch kann Arrays nicht granular modifizieren. Ein Array-Feld im Body ersetzt immer das gesamte Array in der Ressource. Wenn ein Client ein Element zu einem Array hinzufügen oder entfernen will, ohne die anderen Elemente zu beeinflussen, muss er das gesamte aktuelle Array laden, modifizieren und zurücksenden. Das ist das Lost-Update-Problem, das PATCH eigentlich lösen soll – bei Arrays löst Merge Patch es nicht.
5. JSON Patch (RFC 6902): mächtig und präzise
JSON Patch ist der mächtigere und präzisere PATCH-Standard. Statt einer partiellen Ressource sendet der Client eine Liste von Operationen, die auf die Ressource angewendet werden sollen. Jede Operation hat einen Typ (add, remove, replace, move, copy, test), einen Pfad (JSON Pointer nach RFC 6901) und ggf. einen Wert.
JSON Patch löst alle Probleme, die Merge Patch hat: Felder können auf null gesetzt werden (mit replace), Arrays können granular modifiziert werden (mit add und remove auf Array-Indices), und die test-Operation ermöglicht optimistisches Locking: Wenn ein Feld vor der Änderung den erwarteten Wert hat, wird die Operation angewendet; wenn nicht, schlägt der gesamte Patch atomar fehl. Der Preis für diese Mächtigkeit: Eine komplexere Syntax, die für einfache Anwendungsfälle überdimensioniert ist.
[
{
"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 Operationen im Detail
Die sechs JSON Patch Operationen decken alle Modifikationsfälle ab. add: Fügt einen Wert an einem Pfad hinzu. Bei Arrays mit Index /- wird am Ende angefügt. remove: Entfernt den Wert am angegebenen Pfad. replace: Ersetzt den Wert (Äquivalent zu remove + add). move: Verschiebt einen Wert von einem Pfad zu einem anderen. copy: Kopiert einen Wert von einem Pfad zu einem anderen. test: Prüft, ob der Wert am angegebenen Pfad dem erwarteten Wert entspricht – schlägt die Prüfung fehl, wird der gesamte Patch atomar zurückgerollt.
Die test-Operation ist das mächtigste Feature von JSON Patch für die Praxis: Es implementiert optimistisches Locking auf Feldebene, ohne eine separate If-Match-Header-Mechanik zu benötigen. Wenn ein Client sicherstellen will, dass das Version-Feld noch den Wert 1 hat, bevor er es auf 2 setzt, schreibt er {"op": "test", "path": "/version", "value": 1} vor die replace-Operation. Wenn ein anderer Client in der Zwischenzeit bereits auf 2 geändert hat, schlägt die test-Operation fehl, und die gesamte Patch-Anfrage gibt 409 Conflict zurück.
7. Merge Patch in OpenAPI dokumentieren
Merge Patch in OpenAPI zu dokumentieren, erfordert besondere Sorgfalt beim Request-Body-Schema. Da Merge Patch alle Felder optional macht und null eine spezielle Bedeutung hat (Feld entfernen), kann das normale Ressourcen-Schema nicht direkt wiederverwendet werden. Stattdessen muss ein separates "Patch-Schema" definiert werden, das alle Felder optional macht und nullable ist – ohne dass nullable-Felder in der API eigentlich nullable wären.
Das Schlüsselelement: Der Content-Type application/merge-patch+json muss explizit im OpenAPI-Dokument als Content-Type des Request-Bodys angegeben werden. Das signalisiert Tooling und Konsumenten, welcher Standard verwendet wird. In der Beschreibung sollte die null-Semantik erklärt werden: "null removes the field from the resource." Viele OpenAPI-Dokumente verwenden hier application/json und verpassen damit die Chance, die Standard-Semantik zu kommunizieren.
# 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. JSON Patch in OpenAPI dokumentieren
JSON Patch in OpenAPI zu dokumentieren, ist straightforward: Der Content-Type ist application/json-patch+json, der Request-Body ist ein Array von Operation-Objekten. Da JSON Patch ein standardisiertes Format hat, kann das Schema aus dem RFC direkt als OpenAPI-Komponente definiert werden. Das Schema ist nicht besonders komplex, aber die Kombination aus oneOf für die verschiedenen Operationstypen und JSON Pointer-Pfade macht es etwas ausführlicher.
Ein pragmatischer Ansatz: Das vollständige JSON-Patch-Schema als reusable Komponente definieren, die in allen PATCH-Endpoints referenziert werden kann, die JSON Patch verwenden. Das vermeidet Wiederholung und stellt sicher, dass alle JSON-Patch-Endpoints dasselbe Schema-Format verwenden. Beispiele in der OpenAPI-Dokumentation sind bei JSON Patch besonders wichtig, weil die Syntax für viele Entwickler ungewohnt ist.
| Kriterium | Merge Patch (RFC 7396) | JSON Patch (RFC 6902) | Empfehlung |
|---|---|---|---|
| Komplexität für Clients | Niedrig (intuitiv) | Mittel (Operation-Syntax) | Merge Patch für einfache Cases |
| null-Semantik | null = Feld entfernen | null = echter null-Wert | JSON Patch bei nullable Feldern |
| Array-Modifikation | Nur kompletter Austausch | add/remove einzelner Items | JSON Patch für Array-Operationen |
| Optimistisches Locking | Nein (via If-Match Header) | Ja (test-Operation) | JSON Patch für Locking |
| Content-Type | application/merge-patch+json | application/json-patch+json | Immer explizit setzen |
# 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. Zusammenfassung
PATCH richtig zu modellieren bedeutet, sich für einen der beiden Standards zu entscheiden und diesen konsequent mit dem richtigen Content-Type zu verwenden. Merge Patch (RFC 7396) ist die richtige Wahl für einfache partielle Updates, bei denen keine Felder nullable sind und keine granularen Array-Operationen gebraucht werden. JSON Patch (RFC 6902) ist die richtige Wahl, wenn nullable Felder, Array-Operationen oder optimistisches Locking benötigt werden.
Beide Standards sollten in OpenAPI mit dem korrekten Content-Type (application/merge-patch+json bzw. application/json-patch+json) dokumentiert werden – nicht mit dem generischen application/json. Die null-Semantik von Merge Patch muss explizit im description-Feld des PATCH-Endpoints erklärt werden. JSON Patch braucht gute Beispiele in der OpenAPI-Dokumentation, weil die Syntax ungewohnt ist. Proprietäre PATCH-Implementierungen ohne Content-Type-Konvention vermeiden – sie erzeugen Ambiguität für Clients und Implementierungsprobleme im Team.
PATCH modellieren — Das Wichtigste auf einen Blick
Merge Patch
RFC 7396. Content-Type: application/merge-patch+json. Einfach, intuitiv. null = Feld entfernen (Fallstrick!). Arrays werden komplett ersetzt. Kein optimistisches Locking.
JSON Patch
RFC 6902. Content-Type: application/json-patch+json. Atomare Operationen. null ist echter null-Wert. Granulare Array-Operationen. test-Operation für optimistisches Locking.
Entscheidungshilfe
Merge Patch für einfache Felder ohne Nullable und ohne Array-Ops. JSON Patch für nullable Felder, Array-Modifikation oder Locking. Proprietäre PATCH-Semantik vermeiden.
OpenAPI
Richtigen Content-Type angeben (nicht application/json). Patch-Schema von Ressourcen-Schema trennen. null-Semantik in description dokumentieren. Beispiele für JSON Patch obligatorisch.