Wenn zwei JSON-Dokumente sich unterscheiden: Positionsdiff, ID-Diff oder erst normalisieren
Fazit: Fünfzig Datensätze werden einundfünfzig, und der Diff meldet einundfünfzig Änderungen. Behandelt Fehlalarme von Textdiffs, das Zuordnen von Arrays nach id, jq-Normalisierung und anwendbare Patches.

Kurz gesagt: Zuerst normalisieren, dann vergleichen – verwenden Sie jq -S oder stabile Serialisierung, um Schlüsselreihenfolge, Einrückung und Zahlenschreibweise zu beseitigen, sortieren oder indexieren Sie Arrays nach einem Geschäftsschlüssel, wenn die Reihenfolge bedeutungslos ist, und verwandeln Sie den Diff erst dann in einen Patch.
Ein JSON-Unterschied ist kein Textunterschied. Vergleichen Sie die Dateien als Strings, und eine Schlüsselumordnung leuchtet die ganze Seite auf; vergleichen Sie Arrays nach Index, und das Einfügen eines Datensatzes markiert die anderen fünfzig als geändert.
Ein Patch wurde an den Bestellservice ausgeliefert, und Sie haben nur zwei Antworten: fünfzig Datensätze vorher, einundfünfzig nachher, mit einem neuen Datensatz in der Mitte eingefügt. Das Diff-Werkzeug meldet einundfünfzig Änderungen, eine Wand aus Rot, und Sie können immer noch nicht sagen, welcher Datensatz sich tatsächlich bewegt hat.
Das Werkzeug ist nicht kaputt. „Vergleichen“ bedeutet bei JSON drei verschiedene Dinge, und die meisten greifen standardmäßig zum am wenigsten nützlichen.
Den Fehlalarm reproduzieren: eine Einfügung, einundfünfzig Änderungen
Dreißig Zeilen Node 22: fünfzig Bestellungen bauen ({id, sku, qty}), dann eine ans Ende anhängen und eine an den Anfang einfügen und Unterschiede mit Index-für-Index-Ausrichtung zählen.
| Operation | Datensätze | Gemeldete Änderungen nach Position |
|---|---|---|
| Eine ans Ende anhängen | 50 → 51 | 1 |
| Eine an den Anfang einfügen | 50 → 51 | 51 |
Derselbe „ein zusätzlicher Datensatz“, einundfünfzig Mal das Rauschen. Der Grund ist banal: Beim Vergleich nach Index landet das neue Element an Position 0 und jeder bestehende Datensatz rückt einen Slot nach unten. Position 0 gegen Position 0, Position 1 gegen Position 1 – kein Paar ist gleich.
Bauen Sie stattdessen eine nach id geschlüsselte Map, und dieselben Daten melden genau einen Eintrag: $[id=ORD-999] added.
Dieses Experiment liefert die Regel, die wichtiger ist als jede Werkzeugempfehlung: ob das Array eine geordnete Sequenz oder eine ungeordnete Menge ist, entscheidet, welchen Vergleich Sie verwenden sollten.
Drei Arten, wie ein Textdiff über JSON lügt
diff a.json b.json vergleicht Zeichen. JSON trägt eine Datenstruktur. Diese Diskrepanz erzeugt drei zuverlässige Arten von Fehlalarmen.
1. Umgeordnete Objektschlüssel
RFC 8259 Abschnitt 4 ist ausdrücklich: Ein Objekt ist „eine ungeordnete Sammlung von Name/Wert-Paaren“. Diese beiden Dokumente sind derselbe Wert:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
Ein Zeilendiff nennt alle drei Zeilen geändert. Ich habe die Schlüsselreihenfolge eines Objekts mit zehn Schlüsseln umgekehrt, beide hübsch formatiert und zeilenweise verglichen: alle zehn Zeilen markiert, während ein struktureller Vergleich sie als gleich bezeichnet. Rauschverhältnis 10:0.
Warum ändert sich die Schlüsselreihenfolge überhaupt? Serializer wechseln, einen Formatter mit sortierten Schlüsseln laufen lassen, das Objekt aus einem Dictionary oder einer Hash-Map neu bauen, zwischen Sprachlaufzeiten wechseln – jedes davon genügt, um identische Daten auf der Platte unterschiedlich aussehen zu lassen.
2. Whitespace und Einrückung
{"a":1} und {\n "a": 1\n} parsen zum selben Wert. Ein Textdiff sieht Zeilenumbrüche und Leerzeichen und markiert alles.
3. Verschiedene Schreibweisen von Zahlen und Escapes
1.0 und 1, 1e2 und 100, "\u00e9" und "é" – derselbe Wert nach dem Parsen, unterschiedlicher Text nach dem Serialisieren. Dieser Fall ist milder, weil er wenige Zeilen berührt, aber in Konfigurationsreviews genügt er, um zu glauben, eine Zahl habe sich geändert.
Der umgekehrte Fall ist das, was Text nicht sehen und Struktur sehen kann: 5 versus "5". Ein zusätzliches Anführungszeichenpaar, aber der Typ ist gedriftet – meist ein Bug statt eines Updates. Ein struktureller Vergleich meldet es ohne Zögern als modify.
Drei Vergleichsmodi, drei Aufgaben
| Modus | Was verglichen wird | Rauschen kommt von | Ausgabe | Passt zu |
|---|---|---|---|---|
| Textdiff | Zeichenzeilen | Schlüsselreihenfolge, Einrückung, Zahlenschreibweise | Zeilennummern, Rot/Grün | Lokalisierungsbundles, Fixtures mit stabiler Schlüsselreihenfolge |
| Struktureller Diff | Geparste Bäume, nach Pfad ausgerichtet | Arrays nach Index ausgerichtet | Pfade mit add/remove/modify | API-Antworten, Konfigurationen, Webhooks |
| Semantischer Diff | Bäume plus Geschäftsschlüssel | Nur eine schlechte Schlüsselwahl | Pfade mit move/insert-Erkennung | Datensatzlisten, Bestellungen, Bestand |
Der strukturelle Diff löst die Schicht „Schlüsselreihenfolge und Formatierung“. Der semantische Diff geht einen Schritt weiter und löst „Identität von Array-Elementen“. Der Unterschied ist nicht die Implementierungskomplexität; er ist, ob Sie bereit sind, dem Werkzeug zu sagen, welches Feld der Pass eines Datensatzes ist.
Arrays sind die Trennlinie
Stellen Sie eine Frage: Ist die Reihenfolge dieses Arrays selbst Daten?
- Reihenfolge ist Daten: Migrationsschritte, Suchrankings, Diagrammpunkte, Genehmigungsketten. Eine Positionsänderung ist eine Verhaltensänderung, Positionsvergleich ist also korrekt.
- Reihenfolge ist bedeutungslos: Tags, Berechtigungs-ID-Mengen, Bestellpositionen, Bestandslisten. Eine Einfügung sollte nicht beeinflussen, wie andere Einträge beurteilt werden.
Ist die Reihenfolge bedeutungslos, indexieren Sie die Datensätze nach einem Schlüssel, statt sie nach Position zu bearbeiten:
// Match by id: an insert or delete reports one entry, other items stay put
function diffById(before, after, path = '$') {
const out = [];
const beforeMap = new Map(before.map((item) => [item.id, item]));
const afterMap = new Map(after.map((item) => [item.id, item]));
for (const [id, value] of afterMap) {
if (!beforeMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'added' });
}
for (const [id, value] of beforeMap) {
if (!afterMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'removed' });
else if (JSON.stringify(value) !== JSON.stringify(afterMap.get(id))) {
out.push({ path: `${path}[id=${id}]`, type: 'changed' });
}
}
return out;
}
Ausgereifte Bibliotheken machen das zu einer Option. jsondiffpatch verwendet einen LCS, um die längste gemeinsame Teilsequenz zu finden, aber Objektelemente innerhalb von Arrays werden standardmäßig nach Position zugeordnet – die offizielle Dokumentation betont, dass Sie eine objectHash-Funktion liefern müssen, um sie nach Inhalt zuzuordnen. Erstnutzer schließen oft, die Bibliothek sei dumm; in Wahrheit haben sie nie einen Schlüssel übergeben.
Auch die Schlüsselwahl zählt. id ist die sichere Wahl. sku funktioniert nur, wenn SKUs eindeutig sind. Das ganze Objekt zu hashen verwandelt „ein Feld geändert“ in „eins entfernt plus eins hinzugefügt“, was schlechter als Positionsvergleich ist.
Der schnellste Terminal-Trick: mit jq normalisieren
Wollen Sie keine Bibliothek und nur einen Blick, normalisieren Sie zuerst und diffen dann. jq -S sortiert Objektschlüssel rekursiv und wendet eine konsistente Einrückung an:
# Normalize, then compare line by line; key order and indentation noise disappear
diff -u <(jq -S . before.json) <(jq -S . after.json)
# When the array is genuinely unordered, sort by key before comparing
diff -u <(jq -S '.items |= sort_by(.id)' before.json) \
<(jq -S '.items |= sort_by(.id)' after.json)
Ich habe printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . ausgeführt und {"a":1,"arr":[1,2],"b":2} erhalten – Schlüssel sortiert, arr unangetastet. Merken Sie sich das: -S sortiert nur Objektschlüssel, nie Array-Elemente. Entweder Arrays ausdrücklich mit sort / sort_by sortieren oder Positionsrauschen akzeptieren.
Zwei Fallen:
jqkann nicht in place bearbeiten; es schreibt auf die Standardausgabe. Verwenden Sie Prozesssubstitution (<(...)), oder schreiben Sie eine Temp-Datei, prüfen Sie und ersetzen Sie dann;-rentfernt Anführungszeichen von Strings, die Ausgabe kann also Zeilenumbrüche oder shell-relevante Zeichen enthalten. Quotieren Sie nicht vertrauenswürdige Daten, bevor Sie sie weiterleiten.
Was /diff dieser Website tut und was nicht
Das eingebaute JSON-Vergleichswerkzeug folgt demselben strukturellen Ansatz: Beide Seiten werden in Bäume geparst und rekursiv nach Pfad ausgerichtet, was path + type(add/remove/modify) + before/after ergibt. Die Objektschlüsselreihenfolge beeinflusst das Ergebnis nicht, und alles läuft im Browser – eingefügte Payloads werden nie hochgeladen.
Seine Arrays werden jedoch nach Index ausgerichtet. Das ist eine bewusste Abwägung, kein Defekt:
- Am Ende anhängen, dieselbe Position ändern oder als Ganzes ersetzen ergibt saubere Ausgabe;
- Einen Datensatz am Anfang oder in der Mitte einfügen markiert alles danach als
modify, und Sie müssen erkennen, dass „eigentlich nur ein Datensatz neu ist“.
Wenn Sie Bestelllisten vergleichen, besteht der richtige Schritt also darin, beide Seiten vor dem Einfügen nach id zu sortieren. Sortierte Eingabe fixiert den Einfügepunkt und kollabiert das Rauschen zu einem Eintrag. Keine Installation nötig, und es deckt die meisten Alltagsfälle ab.
Brauchen Sie „einen anwendbaren Patch generieren“ oder „verschobene Elemente erkennen“, tut das In-Page-Werkzeug das nicht. Das ist der nächste Abschnitt.
Einen Diff in einen Patch verwandeln: zwei Formate, nicht mischen
Menschen lesen Diffs; Programme wenden Patches an. JSON hat zwei Patch-Formate mit völlig unterschiedlicher Semantik.
| Dimension | JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) |
|---|---|---|
| Form | Array von Operationen mit op, path, value |
Ein JSON-Objekt, geformt wie das Zieldokument |
| Arrays | Ja, nach Index, /items/0, /- zum Anhängen |
Nein, Arrays werden als Ganzes ersetzt |
| Einen Schlüssel löschen | remove |
Nur über null, was ein Feld, dessen Wert legitim null ist, nicht löschen kann |
| Eine Verschiebung ausdrücken | move / copy |
Nein |
| Passt zu | Ressourcen mit Arrays, Audit-Logs, atomare Updates | Partielle Updates einfacher Objekte, Konfigurationsauslieferung |
JSON Patch hat eine Operation, die Leute überspringen: test.
[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/status", "value": "shipped" }
]
Scheitert test, wird der ganze Patch abgelehnt. Das ist optimistische Nebenläufigkeit, eingebaut ins Protokoll – kein zusätzlicher If-Match-Header nötig, um „ich habe nur geschrieben, weil ich Version 3 gesehen habe“ zu garantieren. Für einen PATCH-Endpunkt ist das mehr wert als jede Dokumentationskonvention.
Pfade verwenden JSON Pointer (RFC 6901): ~0 escaped ~, ~1 escaped /. Schlüssel mit Schrägstrichen (etwa MIME-Typen als Schlüssel) müssen escaped werden, sonst landet der Pointer woanders. Die Details stehen in JSON Pointer und Patch.
Ein praktisches Problem bleibt: automatisch aus einem Diff generierte Patches adressieren Arrays meist nach Index. Indizes sind in der Produktion fragil – eine gleichzeitige Einfügung und /items/3 zeigt nicht mehr auf denselben Datensatz. Beschränken Sie generierte Patches entweder auf Objektfelder oder validieren Sie die kritischen Felder mit test, bevor Sie sie anwenden.
Den Vergleich in CI bringen: Werte prüfen, nicht Text
Drei Regeln genügen:
- In Tests geparste Werte prüfen, niemals die Ausgabe von
JSON.stringify. In JesttoEqualverwenden; in Python nachjson.loadsassert a == b. Schlüsselreihenfolge und Einrückung nehmen nicht teil. - Dynamische Felder whitelisten.
requestId,timestamp,traceIdunterscheiden sich bei jedem Lauf; ohne Ausschlüsse ist der Test dauerhaft rot.propertyFilterin jsondiffpatch undexclude_pathsin deepdiff existieren dafür. - Bei Fehlern Pfade ausgeben, nicht das ganze Dokument. Niemand liest 5 MB Payload in einem Log;
$.data.items[id=ORD-042].qty: 2 → 3lokalisiert das Problem sofort.
Konfigurationsdrift funktioniert genauso: jq -S auf Staging- und Produktionskonfiguration anwenden, Arrays nach einem Geschäftsschlüssel sortieren, und was bleibt, ist ein echter Unterschied. Diese Problemklasse – dieselben Daten sehen in verschiedenen Umgebungen unterschiedlich aus – ist meist ein undefinierter Vertrag, worum es in API-Vertragsdesign geht.
FAQ
Zählt die Schlüsselreihenfolge als Unterschied?
Nein. RFC 8259 definiert Objekte als ungeordnete Sammlungen, {"a":1,"b":2} und {"b":2,"a":1} sind also derselbe Wert. Jedes Werkzeug, das eine Umordnung als Änderung meldet, macht Textvergleich, keinen JSON-Vergleich.
Warum markiert das Einfügen eines Datensatzes jeden Eintrag als geändert?
Weil das Werkzeug Arrays Index für Index ausrichtet. Das neue Element sitzt an Position 0, jeder bestehende Datensatz rückt einen nach unten, und keine zwei Positionen passen. Sortieren oder indexieren Sie vor dem Vergleich nach einem Schlüssel wie id, statt nach Position zu vergleichen.
Laden Online-Vergleichswerkzeuge meine Daten hoch?
Das hängt von der Implementierung ab. Serverseitig gerenderte Werkzeuge schicken die Payload an ein Backend, wo sie protokolliert werden kann; clientseitige Werkzeuge parsen im Browser und laden nichts hoch. Bevor Sie Produktionskonfigurationen oder Nutzer-Payloads vergleichen, bestätigen Sie, dass die Seite lokal rechnet, oder trennen Sie das Netzwerk und versuchen es erneut.
Kann ein Diff-Ergebnis direkt als Patch verwendet werden?
Nicht direkt. Ein Diff sagt, wo Dinge sich unterscheiden; JSON Patch sagt, welche Operationen in welcher Reihenfolge auszuführen sind. Dazwischen sitzt eine Konvertierungsschicht. Generierte Patches adressieren Arrays zudem meist nach Index, was unter Nebenläufigkeit bricht – ergänzen Sie test-Operationen, bevor Sie sie anwenden.
Wie vergleiche ich Dateien von zig Megabyte?
Zuerst streamen (jq -c schreibt einen Datensatz pro Zeile), oder extrahieren Sie nur den Teilbaum, der Sie interessiert, etwa jq '.data.items'. Ein vollständiger rekursiver Vergleich von zig Megabyte passt noch in den Speicher; jenseits von hundert Megabyte shardieren Sie oder indexieren nach Schlüssel. Zu konkreten Speicher- und Zeitwerten siehe Performance bei großen JSON-Dateien.
Sollte ich Arrays vor dem Vergleich nach id sortieren?
Ja. Das eingebaute JSON-Vergleichswerkzeug richtet Arrays nach Index aus, das Einfügen eines Datensatzes am Anfang oder in der Mitte markiert also alles danach als modify; beide Seiten vor dem Einfügen nach id zu sortieren fixiert den Einfügepunkt und kollabiert das Rauschen zu einem Eintrag. Die Ausnahme ist, wenn die Array-Reihenfolge selbst Daten sind – Migrationsschritte, Suchrankings, Diagrammpunkte –, wo Positionsvergleich korrekt und Sortieren falsch ist.