JSON Toolkit

JSON Pointer, Patch und Merge Patch

Fazit: Drei kurze Spezifikationen bilden ein Änderungsprotokoll: Pointer lokalisiert, Patch bearbeitet präzise, Merge Patch führt teilweise zusammen. Eine falsche Wahl schmerzt bei Arrays und Nebenläufigkeit.

Fortgeschritten~12 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für JSON Pointer, Patch und Merge Patch

Kurz gesagt: JSON Pointer zum Lokalisieren nach Position und JSONPath zum Filtern oder Rekursieren verwenden; Array-Einfügen und -Löschen, null-Schreibvorgänge und Versionsprüfungen JSON Patch überlassen und Merge Patch für flache Konfiguration und Formularressourcen reservieren.

Lokalisieren und Ändern sind zwei verschiedene Dinge, und die Spezifikationen teilen sie in drei Dokumente auf. Der übliche Preis, sie zu verwechseln, ist „das ganze Array wurde ersetzt“, und niemand merkt es.

1. JSON Pointer: Die kürzeste Lokalisierungssyntax

RFC 6901 tut genau eine Sache: mit einem String auf einen Knoten in einem Dokument zeigen.

""             → the whole document
"/store"       → the store member under the root
"/store/book"  → one level deeper
"/store/book/0/title" → item 0 of the array

Drei Regeln:

  1. Der leere String zeigt auf den Wurzelknoten selbst, nicht auf einen „leeren Knoten“;
  2. Jedes / trennt ein Referenz-Token; Arrays verwenden dezimale Indizes (führende Nullen sind nicht erlaubt, 01 ist also ungültig);
  3. Innerhalb eines Tokens wird ~ als ~0 und / als ~1 geschrieben – ~ vor / ersetzen, sonst endet man beim doppelten Escaping.
/**
 * Escapes a single reference token into a JSON Pointer segment.
 * @param token Original key name or index
 * @returns Escaped token
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Joins an array of key names into a JSON Pointer.
 * @param tokens Array of key names or indices
 * @returns Pointer string beginning with /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

Der Wert zeigt sich am besten, wenn der Schlüssel selbst / enthält: Der Pointer für {"a/b": 1} ist /a~1b, nicht /a/b (Letzteres bedeutet b unter a).

In einer URL ist außerdem Prozentkodierung nötig, weil # und ~ in einem Fragment besondere Bedeutung haben.

2. JSON Patch: Eine atomare Folge von sechs Operationen

RFC 6902 beschreibt Änderungen mit einem Array von Operationen, Medientyp application/json-patch+json:

[
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/name", "value": "新的名字" },
  { "op": "add", "path": "/tags/-", "value": "json" },
  { "op": "remove", "path": "/temp" },
  { "op": "copy", "from": "/name", "path": "/alias" },
  { "op": "move", "from": "/tags/0", "path": "/tags/1" }
]
Operation Semantik Grenze
add Fügt ein Mitglied hinzu oder ein Array-Element ein Der Array-Index darf der Länge entsprechen (Anhängen); - bedeutet das Ende
remove Entfernt Der Pfad muss existieren
replace Ersetzt einen vorhandenen Wert Der Pfad muss existieren (entspricht remove + add, erfordert aber zuerst einen vorhandenen Pfad)
move Verschiebt Ein Knoten kann nicht unter seinen eigenen Teilpfad verschoben werden
copy Kopiert Die Quelle muss existieren
test Prüft Gleichheit Scheitert sie, wird der ganze Patch nicht angewendet; dient der optimistischen Sperre

Die zentrale Eigenschaft ist Atomarität: Scheitert eine Operation, wird der ganze Patch zurückgerollt, es gibt also keinen Zwischenzustand, in dem „die ersten drei angewendet und die vierte gescheitert ist“. Das macht ihn zu einem großartigen PATCH-Request-Body.

2.1 test für optimistische Nebenläufigkeit nutzen

[
  { "op": "test", "path": "/etag", "value": "a3f9c1" },
  { "op": "replace", "path": "/status", "value": "paid" }
]

Der Client GETet zuerst die etag und schreibt sie in die test-Operation. Bei gleichzeitigen Schreibvorgängen scheitert der test der zweiten Anfrage, und der Server liefert 409 Conflict oder 412 Precondition Failed. Das ist weit sicherer als Last-Write-Wins und braucht keine verteilte Sperre.

2.2 Array-Operationen sind am fehleranfälligsten

// Want to insert at index 1
{ "op": "add", "path": "/items/1", "value": {...} }   // Correct: insert; the original index 1 and everything after shift back

// Want to replace index 1
{ "op": "replace", "path": "/items/1", "value": {...} } // Correct: replace

add an Array-Index n ist ein Einfügen, kein Überschreiben; ein Index außerhalb des Bereichs (n > length) muss einen Fehler auslösen. Manche Implementierungen sind dabei nachsichtig, wodurch derselbe Patch auf verschiedenen Servern unterschiedlich wirkt.

3. Merge Patch: Ein Fragment, das wie das Ziel aussieht

RFC 7386 hat nur vier Regeln:

  1. Das Ziel ist ein Objekt und der Patch ist ein Objekt → rekursiv zusammenführen;
  2. Der Wert eines Schlüssels im Patch ist null → diesen Schlüssel im Ziel löschen;
  3. Der Wert eines Schlüssels im Patch ist kein Objekt → direkt ersetzen;
  4. Der Patch selbst ist kein Objekt → das ganze Ziel ersetzen.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

// Patch
{ "a": "z", "c": { "f": null } }

// Result
{ "a": "z", "c": { "d": "e" } }

Der Medientyp ist application/merge-patch+json. Es liest sich wie ein „partielles Update“, was Missbrauch besonders leicht macht:

  • Arrays lassen sich nur als Ganzes ersetzen. list[2] löschen? Merge Patch kann das nicht;
  • null lässt sich nicht schreiben. null ist bereits mit der Bedeutung „Löschen“ belegt;
  • Zwischen „dieses Feld unangetastet lassen“ und „dieses Feld löschen“ gibt es keinen dritten Zustand (etwa ausdrücklich auf leer setzen).

4. Fähigkeitsmatrix: Welches wählen

Anforderung JSON Patch Merge Patch
Ein bestimmtes Array-Element präzise ersetzen ✓ ✗
Ein Element aus einem Array löschen ✓ ✗
Mehrere Felder teilweise aktualisieren ✓ (mehrere replace) ✓ (am knappsten)
Einen null-Wert schreiben ✓ ✗
Nebenläufigkeitskontrolle (Versionsprüfung) ✓ (test) ✗ (nur über If-Match)
Lesbarkeit des Patches Mittel Gut (sieht aus wie die Daten selbst)
Implementierungskomplexität Hoch Niedrig
Passende Ressourcenformen Tief verschachtelt, listenartig Flache Konfiguration, Formulare

Faustregeln:

  • Formulare, Einstellungsseiten, Konfigurationsressourcen → Merge Patch; das Frontend sendet nur die geänderten Felder;
  • Hinzufügen und Entfernen in Listen, Zustandsübergänge, alles mit Nebenläufigkeitsschutz → JSON Patch;
  • Beide können koexistieren: verschiedene Endpunkte deklarieren verschiedene Content-Types, und der Server verteilt nach Typ.

5. Umsetzungshinweise

import { applyPatch, compare } from 'fast-json-patch';

const document = { version: 7, items: [{ id: 1 }, { id: 2 }] };
const patch = compare(document, { version: 8, items: [{ id: 1 }] });

// test operations are validated by default; setting mutateDocument to true modifies in place, use with caution
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

Vier leicht übersehene Punkte:

  1. Gemeinsame Objekte nicht in place verändern; ein gescheiterter Patch hinterlässt einen halb veränderten Zustand (es sei denn, die Implementierung garantiert Rollback);
  2. Pfadvalidierung: Patches von außen müssen jeden path/from validieren und Schlüssel wie __proto__ und constructor ablehnen;
  3. Größenlimits: die Zahl der Patch-Einträge begrenzen, damit ein einzelner Patch nicht Hunderttausende Operationen ausführt;
  4. Auditierung: Patches eignen sich naturgemäß für Audit-Logs – sie sind eine vollständige Beschreibung von „was sich geändert hat“.

6. Fehlersemantik

Situation Vorgeschlagener Statuscode Erklärung
Der Patch ist kein Array / strukturell ungültig 400 Bad Request Das Request-Body-Format ist falsch
Der von path referenzierte Knoten existiert nicht 422 Unprocessable Entity Syntaktisch gültig, aber semantisch nicht ausführbar
Eine test-Operation ist gescheitert 409 oder 412 Zeigt einen Versionskonflikt
Keine Berechtigung, diesen Pfad zu ändern 403 Forbidden Nicht mit 404 verschleiern, außer Sie wollen die Existenz der Ressource bewusst verbergen

400 von 422 zu unterscheiden ist wichtig: Ersteres heißt „ich kann deinen Patch nicht verstehen“, Letzteres „ich verstehe ihn, aber diese Daten erlauben diese Änderung nicht“.

7. Checkliste

  • [ ] Der Endpunkt deklariert den richtigen Content-Type (json-patch+json / merge-patch+json);
  • [ ] Das Anwenden des Patches ist atomar, und ein Fehler hinterlässt kein Teilresultat;
  • [ ] Externe Patches werden gegen eine Pfad-Whitelist und ein Eintragszahl-Limit validiert;
  • [ ] Die Nebenläufigkeitskontrolle verwendet test oder If-Match;
  • [ ] Patches werden ins Audit-Log geschrieben;
  • [ ] Array-Szenarien wählen ausdrücklich JSON Patch; verlassen Sie sich nicht auf Merge Patch.

FAQ

Wie entscheide ich zwischen JSON Patch und Merge Patch?

Verwenden Sie JSON Patch, wenn Sie ein bestimmtes Array-Element ersetzen oder entfernen, einen null-Wert schreiben oder eine Version prüfen müssen; verwenden Sie Merge Patch für Formulare und Konfigurationsressourcen, die nur wenige Felder aktualisieren. Merge Patch kann kein einzelnes Array-Element löschen und kein null schreiben, weil null bereits Löschen bedeutet.

Warum ist add an Array-Index n kein Überschreiben?

Weil add in ein Array einfügt: Das Element an Index n und alles danach rücken nach hinten. Um ein vorhandenes Element zu ersetzen, müssen Sie replace verwenden, und der Pfad muss zuerst existieren. Ein Index außerhalb des Bereichs (n > length) sollte einen Fehler auslösen; nachsichtige Implementierungen lassen denselben Patch auf verschiedenen Servern unterschiedlich wirken.

Sollte ich das Originalobjekt in place verändern?

Nein. Ein gescheiterter Patch hinterlässt einen halb veränderten Zustand, es sei denn, die Implementierung garantiert Rollback. Wenden Sie den Patch auf eine tief kopierte oder strukturierte Klone an und tauschen Sie die Referenz nach Erfolg.