JSON Patch in der Produktion: Atomarität, Idempotenz und Konflikte
Fazit: JSON Patch ist nur ein Änderungsformat; Atomarität, Idempotenz und Nebenläufigkeit bleiben Ihre Aufgabe. Behandelt test-Vorbedingungen, Pfad-Escaping, Rollback und ETag-Koordination.

Kurz gesagt: Zuerst die Zielpfade und Berechtigungen des Patches validieren, dann mit test oder If-Match eine Nebenläufigkeitsprüfung ergänzen; wenden Sie den Patch auf eine Kopie an, damit ein Fehler nichts halb Geschriebenes hinterlässt.
Die echte Gefahr einer
PATCH-Anfrage ist nicht, das falsche Feld zu schreiben. Sie ist zu glauben, das Schreiben sei erfolgreich gewesen, während stillschweigend die Änderung eines anderen überschrieben wurde. JSON Patch gibt Ihnen die Werkzeuge, aber die Sicherheitsplanung bleibt Ihre.
1. Ein kurzer Überblick über die RFC-6902-Operationsmenge
JSON Patch beschreibt Änderungen als Array von Operationen, mit dem Medientyp application/json-patch+json. Sechs Operationen werden in Array-Reihenfolge ausgeführt:
| Operation | Wirkung | Zentrale Einschränkung |
|---|---|---|
add |
Ein Mitglied hinzufügen oder ein Array-Element einfügen | Ein Array-Index darf der Länge entsprechen; - bedeutet das Ende |
remove |
Ein Mitglied oder Element entfernen | Der Zielpfad muss existieren |
replace |
Einen vorhandenen Wert ersetzen | Der Zielpfad muss existieren |
move |
Einen Knoten verschieben | Ein Knoten kann nicht in seinen eigenen Kindpfad verschoben werden |
copy |
Einen Knoten kopieren | Der Quellpfad muss existieren |
test |
Wertgleichheit prüfen | Scheitert sie, wird der ganze Patch nicht angewendet |
Viele erinnern sich nur an die ersten drei und vergessen, dass test aus einem blinden Schreiben ein bedingtes Schreiben macht. Die vollständige Operationssemantik und Array-Grenzen behandelt JSON Pointer, Patch und Merge Patch.
2. Mit test einen Patch zu einem bedingten Update machen
Ein typischer Optimistic-Locking-Patch beginnt nicht mit einem Schreiben, sondern mit einer Versionsprüfung:
{"op":"test","path":"/version","value":1}
In einem vollständigen Patch:
[
{ "op": "test", "path": "/version", "value": 1 },
{ "op": "replace", "path": "/version", "value": 2 },
{ "op": "replace", "path": "/status", "value": "paid" }
]
Die Logik: bestätigen, dass die Dokumentversion aktuell 1 ist, und sie dann auf 2 ändern. Hat eine andere Anfrage die Version bereits auf 2 erhöht, scheitert der erste test, der ganze Patch wird nicht angewendet, und der Server meldet einen Konflikt. Das verhindert verlorene Updates ohne Einführung einer verteilten Sperre.
Einige Designpunkte:
testvor jedes Schreiben setzen. Hat ein früheres Schreiben bereits gewirkt, ist die Prüfung bedeutungslos;- Auf eine vom Server erzeugte Versionsnummer oder einen Digest prüfen, nicht auf ein gewöhnliches Feld, das der Client frei ändern kann;
- Die Version bei jedem erfolgreichen Schreiben erhöhen; sonst bestehen zwei Anfragen mit derselben alten Version beide;
- Der
valuevontestmuss exakt denselben JSON-Typ wie der Dokumentwert haben.1und"1"sind nicht gleich.
3. Pfad-Escaping und Prototype Pollution
Die Escaping-Regeln von JSON Pointer sind einfach: ~ als ~0 und / als ~1 schreiben und ~ vor / ersetzen, sonst entsteht doppeltes Escaping.
Key "a/b" → /a~1b
Key "m~n" → /m~0n
Key "a~/b" → /a~0~1b
Das Risiko externer Patches ist meist nicht das Escaping, sondern gefährliche Schlüssel. In JavaScript können Schlüssel wie __proto__, constructor und prototype Prototype Pollution auslösen, selbst über ein gewöhnliches add:
[
{ "op": "add", "path": "/__proto__/polluted", "value": true }
]
Verwenden Sie eine Allowlist statt einer Denylist:
- Erlauben Sie Patches nur, Geschäftsfelder zu ändern, etwa
/statusoder/items/0/quantity; - Validieren Sie jeden
pathundfromgegen einen regulären Ausdruck oder ein Pfadpräfix und lehnen Sie unerwartete Ebenen außer Array-Indizes ab; - Parsen Sie JSON in
Object.create(null)oderMap-Container, um die Auswirkung einer verschmutzten Prototypenkette zu verringern; - Begrenzen Sie die Zahl der Operationen und die Bytegröße jedes
value.
4. Atomares Anwenden und Rollback
RFC 6902 verlangt atomare Ausführung, aber Atomarität ist eine semantische Anforderung, kein Standard in jeder Bibliothek. Bibliotheken wie fast-json-patch verändern das Dokument in place, wenn mutateDocument true ist, und ein Fehler kann Teile der Änderungen hinterlassen.
import { applyPatch, validate } from 'fast-json-patch';
/**
* Apply a patch immutably and return the original document on failure.
* @param document The original document, which callers must not mutate
* @param patch The JSON Patch operation array to apply
* @returns The result, including a success flag and the new document
*/
function applySafely(document: unknown, patch: unknown[]) {
const errors = validate(patch, document);
if (errors) {
return { ok: false, document };
}
try {
const result = applyPatch(document, patch, false, false);
return { ok: true, document: result.newDocument };
} catch {
return { ok: false, document };
}
}
Drei Umsetzungsempfehlungen:
- Zuerst tief kopieren oder strukturiert klonen, auf der Kopie anwenden und die Referenz erst nach Erfolg austauschen;
- Nicht auf automatischen Rollback einer Bibliothek verlassen; das Verhalten bei Ausnahmen unterscheidet sich zwischen Implementierungen;
- Vor der Ausführung validieren:
validateprüft nur die Struktur, während semantische Konflikte weiterhintestbrauchen; keines ersetzt das andere.
Überspannt ein Patch mehrere Ressourcen, etwa eine Bestellung und ihren Bestand gemeinsam zu ändern, gilt die Atomarität von JSON Patch nur innerhalb eines einzelnen Dokuments. Über Dokumente hinweg brauchen Sie eine Datenbanktransaktion oder kompensierende Aktionen.
5. Idempotenz: PATCH ist nicht von Natur aus idempotent
HTTP-Semantik definiert PUT und DELETE als idempotent, PATCH aber nicht unbedingt. Denselben JSON Patch zu wiederholen kann völlig unterschiedliche Ergebnisse erzeugen:
add /items/-hängt jedes Mal ans Array-Ende an, Wiederholungen erzeugen also Duplikate;replace /versionändert Version 1 zu 2 und gelingt bei einer Wiederholung erneut, obwohl die Operation bereits angewendet wurde;remove /tempscheitert beim zweiten Lauf, weil der Pfad nicht mehr existiert.
Es gibt drei übliche Wege zur Idempotenz:
| Ansatz | So funktioniert es | Am besten für |
|---|---|---|
| Versionsvorbedingung | Erste Operation ist test /version, dann erhöhen |
Ressourcen mit ausdrücklicher Version |
| Idempotenzschlüssel | Client sendet Idempotency-Key; Server cached das Ergebnis |
Zahlungen, Bestellungen und andere Schreibvorgänge |
| Zielzustands-Patch | replace statt add, sodass das Ergebnis nur vom Endwert abhängt |
Konfigurations- und Statusfelder |
Das schlechteste Muster ist ein Patch, der add und remove mischt und dabei annimmt, Wiederholungen seien sicher. Hängt ein Patch etwas an oder löscht etwas, muss er mit einer Versionsprüfung oder einem Idempotenzschlüssel gepaart werden.
6. Nebenläufige Konflikte: ETag und If-Match
Neben test innerhalb des Patches bietet HTTP einen Standardmechanismus: Der Server liefert ein ETag, und der Client sendet bei der nächsten Anfrage If-Match.
GET /orders/42 HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
ETag: "a3f9c1"
Content-Type: application/json
PATCH /orders/42 HTTP/1.1
Host: api.example.com
If-Match: "a3f9c1"
Content-Type: application/json-patch+json
[{"op":"replace","path":"/status","value":"paid"}]
Hat sich die Ressource geändert, liefert der Server 412 Precondition Failed, und der Client lädt neu, bevor er es erneut versucht. If-Match: * bedeutet „ausführen, solange die Ressource existiert“, was nicht passt, wenn Sie Überschreiben verhindern müssen.
Der Unterschied zwischen test und If-Match:
If-Matchwird auf HTTP-Ebene vor dem Body-Parsing geprüft, ist also billiger;testwird auf der Patch-Semantikebene geprüft und kann jedes Feld prüfen, ohne von serverseitiger ETag-Unterstützung abzuhängen;- Beide lassen sich kombinieren, aber implementieren Sie nicht nur eines und behaupten dann, der Endpunkt sei unter Nebenläufigkeit sicher.
7. Bibliotheksunterschiede und Auswahl
| Implementierung | Sprache | Atomarität | test-Unterstützung |
Hinweise |
|---|---|---|---|---|
fast-json-patch |
JavaScript | Optional (gibt standardmäßig ein neues Dokument zurück) | Ja | Breites Ökosystem; auf mutateDocument achten |
jsonpatch |
Python | Rollt bei Ausnahmen zurück | Ja | Nahe am Standard, mit klaren Fehlern |
json-patch |
Go | Arbeitet auf einer Kopie | Ja | Oft zusammen mit JSON Merge Patch |
zjsonpatch |
Java | Ja | Ja | Praktische Jackson-Integration |
Wählen Sie eine Bibliothek nicht allein nach „unterstützt alle sechs Operationen“. Bestätigen Sie drei Dinge: ob ein Fehler zurückrollt, ob test strikt vergleicht und ob ein Array-add außerhalb des Bereichs einen Fehler auslöst. Nachsichtige Implementierungen lassen denselben Patch in verschiedenen Umgebungen unterschiedlich wirken.
8. Eine Sicherheitscheckliste vor dem Start
- [ ] Der Endpunkt deklariert
application/json-patch+json; - [ ] Patches werden auf eine Kopie angewendet, und ein Fehler verunreinigt nie das Originaldokument;
- [ ] Jeder
pathundfromin externen Patches besteht eine Allowlist-Prüfung; - [ ] Gefährliche Schlüssel wie
__proto__,constructorundprototypewerden abgelehnt; - [ ] Operationszahl und Größe eines einzelnen Werts sind begrenzt;
- [ ]
testoderIf-Matchbietet Nebenläufigkeitskontrolle; - [ ] Patches werden mit Akteur, Zeitstempel und dem vollständigen Operationsarray ins Audit-Log geschrieben;
- [ ] Transaktionsgrenzen sind ausdrücklich, wenn ein Patch Dokumente überspannt.
Ein Patch ist von Natur aus eine vollständige Beschreibung von „was sich geändert hat“, was ihn zu einem natürlichen Audit-Datensatz macht. Zu Fehlercodes und Idempotenz auf API-Ebene lesen Sie weiter in JSON-API-Design. Stammt ein Patch aus dem Zusammenführen zweier Branches, brauchen Sie auch Konfliktsemantik, behandelt in Git-Merge für JSON-Dateien.
FAQ
Welchen Statuscode sollte eine gescheiterte test-Operation zurückgeben?
Kommt der Konflikt aus einer Versionsprüfung, sind sowohl 409 Conflict als auch 412 Precondition Failed vertretbar. Bevorzugen Sie 412, um konsistent mit If-Match zu bleiben, und geben Sie die aktuelle Version im Response-Body an, damit der Client es erneut versuchen kann.
Kann ein Patch mit add und remove sicher wiederholt werden?
Nicht direkt. add ans Array-Ende dupliziert Daten, und remove scheitert beim zweiten Lauf, weil der Pfad weg ist. Ergänzen Sie eine Versionsvorbedingung oder einen Idempotenzschlüssel, damit Wiederholungen als dieselbe Operation erkannt werden.
Kann JSON Patch dokumentübergreifende Transaktionen garantieren?
Nein. Die Atomarität von RFC 6902 gilt für ein einzelnes JSON-Dokument. Sind Bestellungen, Bestand oder andere Ressourcen beteiligt, verwenden Sie Datenbanktransaktionen, Saga-Kompensation oder eine Message Queue für Konsistenz.
Warum darf ein Patch niemals nach __proto__ schreiben?
Weil die meisten JavaScript-Implementierungen das Dokument in gewöhnlichen Objekten speichern, __proto__ zu schreiben kann die Prototypenkette verändern und jedes Objekt danach beeinflussen. Eine Pfad-Allowlist ist die verlässlichste Verteidigung; verlassen Sie sich nicht nur auf die interne Filterung einer Bibliothek.
Wie mache ich eine PATCH-Anfrage idempotent?
Es gibt drei übliche Ansätze: den Patch mit test /version beginnen und die Version bei Erfolg erhöhen, den Server das Ergebnis für einen Idempotency-Key cachen lassen, oder replace statt add verwenden, sodass das Ergebnis nur vom Endwert abhängt. Hängt ein Patch etwas an oder löscht etwas, muss er mit einer Versionsprüfung oder einem Idempotenzschlüssel gepaart werden; er kann nicht einfach wiederholt werden.
Sollte ich test und If-Match beide verwenden?
Sie müssen nicht beide verwenden, aber nur eines zu implementieren macht einen Endpunkt nicht sicher unter Nebenläufigkeit. If-Match wird auf HTTP-Ebene vor dem Body-Parsing geprüft, ist also billiger; test wird auf der Patch-Semantikebene geprüft und kann jedes Feld prüfen, ohne von serverseitiger ETag-Unterstützung abzuhängen.