Tutorial: Ein JSON prüfen, bevor es in ein Tag wandert

Inhalt
Eine Nutzlast, die ein Parser verweigert, ist ärgerlich und harmlos: Etwas bricht sofort, an einer Stelle, und die Behebung dauert eine Minute. Eine Nutzlast, die ein Parser annimmt und anders liest als gemeint, ist weder das eine noch das andere.
Beide Arten haben dieselbe Quelle. JSON sieht aus wie ein JavaScript-Objekt und ist ein deutlich engeres Format – und die Unterschiede zwischen beiden sind genau die folgende Liste.

Warum ein JavaScript-Objekt kein JSON ist
Alles, was in JSON gültig ist, ist gültiges JavaScript. Umgekehrt gilt das nicht, und in diesem Abstand wohnen die Fehler.
{ // JavaScript, kein JSON
'order_id': 9007199254740993, // einfache Anfuehrungszeichen
total: 129.90, // Schluessel ohne Anfuehrungszeichen
items: [ { sku: "SKU-42", qty: 2, } ], // Komma vor der Klammer
rabatt: NaN, // kein JSON-Wert
/* ein Kommentar */
}
JSON erlaubt doppelte Anführungszeichen und sonst keine, verlangt für jeden Schlüssel Anführungszeichen, verbietet ein Komma vor einer schliessenden Klammer, kennt keine Kommentare und genau sieben Wertarten: Objekt, Liste, Zeichenkette, Zahl, true, false und null. Es gibt kein undefined, kein NaN, kein Infinity und kein Datum.
Der letzte Punkt gehört klar ausgesprochen, denn er erzeugt eine wiederkehrende Überraschung: Ein Datum hat keine eigene Darstellung. Was ankommt, ist eine Zeichenkette oder eine Zahl, die beide Seiten als Datum zu lesen vereinbart haben – und jede Uneinigkeit über das Format ist für den Parser unsichtbar.
Die fünf Fehler mit Ausnahme
Das sind die lauten, und jeder davon erzeugt dieselbe wenig hilfreiche Meldung über eine unerwartete Marke.
| Fehler | Wie er aussieht |
|---|---|
| Einfache Anführungszeichen | ‘order_id’ statt “order_id” |
| Schlüssel ohne Anführungszeichen | total: 129.90 statt “total”: 129.90 |
| Komma vor der Klammer | Ein Komma vor } oder ] |
| Kommentar | Alles nach // oder zwischen /* und */ |
| Roher Umbruch in einer Zeichenkette | Ein Zeilenumbruch zwischen Anführungszeichen; nötig ist \n |
Ein sechster gehört in dieselbe Gruppe und ist überhaupt nicht sichtbar: eine Byte-Reihenfolge-Marke am Dateianfang. Das sind drei unsichtbare Bytes vor der öffnenden Klammer, und der Parser meldet eine unerwartete Marke an Position 0 – was so aussieht, als sei die Klammer das Problem. Eine Datei, die fehlerfrei aussieht und an Position 0 scheitert, hat eine, und ein Editor mit der Einstellung „ohne Marke speichern” entfernt sie.
Die zwei, die nichts melden
Nach diesen lohnt sich absichtlich zu suchen, denn nichts sonst wird darauf zeigen.
Der erste ist ein doppelter Schlüssel. Die Spezifikation erlaubt ihn, und das übliche Verhalten ist, dass der letzte gewinnt – ein Dokument mit zweimal "total" liest sich also sauber und trägt den zweiten Wert. Liegen die beiden in einer langen Nutzlast weit auseinander, entsteht eine Zahl, die weder falsch aussieht noch richtig ist.
{ "total": 129.90, "currency": "EUR", "total": 139.90 }
JSON.parse(...).total → 139.90
Der zweite ist eine grosse ganze Zahl. Zahlen sind in JavaScript Gleitkommazahlen, und ganze Zahlen bleiben nur bis 2^53 − 1 genau. Eine Bestellnummer, eine Kennung aus einer Datenbank, eine Meta-CAPI-Kennung – sie alle überschreiten das regelmässig, und der Parser rundet sie wortlos.
JSON.parse('{"id": 9007199254740993}').id → 9007199254740992
JSON.parse('{"id": "9007199254740993"}').id → "9007199254740993"
Die Regel daraus ist kurz: Eine Kennung ist eine Zeichenkette, immer, auch wenn sie nur aus Ziffern besteht. Gerechnet wird damit ohnehin nie, und eine Zeichenkette übersteht jeden Parser unverändert.
Die gemeldete Position lesen
Die Position in der Fehlermeldung ist ein Zeichenversatz im ganzen Dokument, und in einer minifizierten Nutzlast ist das eine Zahl ohne Bedeutung.
Der erste Schritt ist deshalb immer, das Dokument zu formatieren, und nicht, den Fehler zu suchen. Eine formatierte Nutzlast legt die Position auf eine Zeile, und die Zeile darüber ist meist die eigentliche Fundstelle – ein fehlendes Komma fällt bei der nächsten Marke auf, nicht dort, wo es fehlt.
try {
JSON.parse(text);
} catch (e) {
const stelle = Number((e.message.match(/position (\d+)/) || [])[1]);
if (!Number.isNaN(stelle)) {
console.log(text.slice(Math.max(0, stelle - 60), stelle + 60));
console.log(" ".repeat(Math.min(60, stelle)) + "^");
}
}
Sechzig Zeichen zu jeder Seite genügen in fast jedem Fall, um das Problem zu sehen, und das Dach nimmt das Abzählen ab. Das lohnt sich als Textbaustein aufzuheben, denn die Alternative besteht darin, einen Formatierer zu öffnen, einzufügen, zu scrollen und die Position zu verlieren.
Auf der Kommandozeile prüfen
Kommt die Nutzlast aus einer Datei oder einer Anfrage, beantwortet ein einziger Befehl die Frage.
# gueltig? formatierte Ausgabe, oder ein Fehler mit Zeilennummer
jq . nutzlast.json
# nur die Antwort, ohne den Inhalt
jq -e . nutzlast.json >/dev/null && echo "gueltig" || echo "ungueltig"
# doppelte Schluessel finden - jq behaelt den letzten, das hier zaehlt sie
jq -r 'paths(scalars) | join(".")' nutzlast.json | sort | uniq -d
# grosse Zahlen, die an Genauigkeit verlieren werden
grep -oE '"[a-z_]+": *[0-9]{16,}' nutzlast.json
Der dritte Befehl lohnt sich auf jeder noch nie geprüften Nutzlast. Er listet die Pfade, die mehr als einmal vorkommen, und das ist der einzige maschinelle Weg zum stillen Fall – bei einer erzeugten Nutzlast findet er überraschend oft etwas, denn eine Vorlage, die ein Feld in zwei Zweigen anhängt, erzeugt genau das.
Wo es am meisten zählt
Drei Orte machen aus einem kleinen JSON-Fehler etwas, das später schwer zu bemerken ist.
Ein JSON-LD-Block in der Seite ist der erste. Ein Syntaxfehler bedeutet, dass Suchmaschinen überhaupt keine strukturierten Daten lesen, und die Seite funktioniert weiterhin einwandfrei – nichts am Auftritt deutet also auf ein Problem hin, und der Verlust zeigt sich Wochen später als ausbleibende erweiterte Treffer. Ein Prüfer auf der ausgelieferten Seite, nicht auf der Vorlage, ist die Kontrolle, die das fängt.
Eine Conversions-API-Nutzlast ist der zweite. Die empfangende Plattform antwortet mit einem Fehler, aber dieser Fehler steht in einer Server-zu-Server-Antwort, die niemand beobachtet, und ein gescheitertes Ereignis erscheint schlicht nicht. Hier richtet der doppelte Schlüssel den grössten Schaden an: Eine Nutzlast mit dem Ereigniswert zweimal sendet den falschen, und Sender wie Empfänger halten die Anfrage für gelungen.
Die Konfiguration einer Tag-Manager-Vorlage ist der dritte. Das JSON darin wird einmal beim Laden der Vorlage gelesen, und ein fehlerhaftes Feld kann die Vorlage installiert, aber wirkungslos zurücklassen. Anders ist dieser Fall darin, dass er beim Import scheitert statt zur Laufzeit – eine bessere Stelle zum Scheitern, und nur dann, wenn jemand die Meldung liest.
Allen dreien gemeinsam: Geprüft wird dort, wo die Nutzlast entsteht, nicht dort, wo sie verbraucht wird. Ein Bauschritt, der den Parser über jede erzeugte Datei laufen lässt, kostet nichts und fängt alle fünf lauten Fehler ab, bevor sie die Maschine verlassen.