JSON Toolkit

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.

Experte~10 Min. LesezeitVeröffentlicht 2026-10-31Aktualisiert 2026-10-31
Titelbild für JSON Patch in der Produktion

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:

  • test vor 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 value von test muss exakt denselben JSON-Typ wie der Dokumentwert haben. 1 und "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:

  1. Erlauben Sie Patches nur, Geschäftsfelder zu ändern, etwa /status oder /items/0/quantity;
  2. Validieren Sie jeden path und from gegen einen regulären Ausdruck oder ein Pfadpräfix und lehnen Sie unerwartete Ebenen außer Array-Indizes ab;
  3. Parsen Sie JSON in Object.create(null) oder Map-Container, um die Auswirkung einer verschmutzten Prototypenkette zu verringern;
  4. 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: validate prüft nur die Struktur, während semantische Konflikte weiterhin test brauchen; 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 /temp scheitert 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-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;
  • 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 path und from in externen Patches besteht eine Allowlist-Prüfung;
  • [ ] Gefährliche Schlüssel wie __proto__, constructor und prototype werden abgelehnt;
  • [ ] Operationszahl und Größe eines einzelnen Werts sind begrenzt;
  • [ ] test oder If-Match bietet 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.