Fehlerhaftes JSON debuggen: ein Feldhandbuch
Die Parse-Meldung ist besser als ihr Ruf. Wie man liest, was V8 heute sagt, die fünf Fehlerarten hinter kaputten Payloads, und die zwei Meldungen ohne Position.
Alle zitieren dieselbe Meldung, wenn sie sich über JSON beschweren: SyntaxError: Unexpected token } in JSON at position 4217. Eine Position, keine Erklärung, viel Glück.
Diese Meldung ist ein Museumsstück. Geben Sie dasselbe kaputte Objekt heute an Node 24, und Sie bekommen dies:
SyntaxError: Expected double-quoted property name in JSON at position 51 (line 4 column 1)
Sie benennt, was der Parser erwartet hat, den Offset sowie Zeile und Spalte. Die Meldung ist gut geworden, während das Gerede darüber schlecht geblieben ist, und die meisten Debugging-Tipps, die Sie finden, sind noch für die alte geschrieben.
Es folgt also eine Anleitung zum Lesen der Meldung, die Sie tatsächlich bekommen, zu den fünf Fehlern hinter fast jedem kaputten Payload, und zu der einen Fehlerfamilie, die immer noch nicht verrät, wo es passiert ist.
Vorher eine Sache ausschliessen: dass die Datei überhaupt kein striktes JSON ist. Konfigurationen für VS Code oder den TypeScript-Compiler sind JSONC, und ein Kommentar oder ein nachgestelltes Komma lässt JSON.parse scheitern, während im Editor alles normal aussieht. Der JSON-Dialekt-Detektor klärt das mit einem Einfügen.
Die Erwartung lesen, nicht nur den Offset
Die Position ist, wo die Grammatik brach. Der Fehler steht meist ein paar Zeichen weiter links, denn die meisten JSON-Fehler sind ein gegebenes und nicht gehaltenes Versprechen: ein Komma, das ein weiteres Element verspricht, ein öffnendes Anführungszeichen, das ein schliessendes verspricht.
"Expected double-quoted property name" heisst, dass etwas einen Property-Namen versprochen hat. Suchen Sie von der genannten Position nach links, bis Sie finden, was das war.
Die fünf üblichen Ursachen
Ein nachgestelltes Komma
Mit weitem Abstand die häufigste. Sie haben das letzte Element gelöscht und sein Komma stehen lassen.
{
"name": "zeroutil",
"category": "developer",
}
JavaScript akzeptiert das. JSON5 akzeptiert das. Striktes JSON nicht.
Finden lassen sie sich mit ,\s*[}\]] in unserem Regex-Tester . Verhindern lassen sie sich mit einem Formatter als Pre-Commit-Hook, denn JSON.stringify erzeugt nie eines. Ein nachgestelltes Komma ist immer ein Artefakt menschlicher Bearbeitung.
Keys ohne oder mit einfachen Anführungszeichen
{
name: "zeroutil",
'category': "developer"
}
Keine der beiden Formen ist JSON, und genau das entsteht, wenn jemand JSON aus dem Gedächtnis tippt. Die Meldung ist hier ungewöhnlich klar: Expected property name or '}' in JSON at position 4 (line 2 column 3), also das n von name. In einer grossen Datei listet grep -nE '^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*[[:space:]]*:' die betroffenen Zeilen.
Nicht maskierte Zeichen in Strings
Drei Dinge müssen in einem JSON-String maskiert werden: das doppelte Anführungszeichen, der Backslash und jedes Steuerzeichen von U+0000 bis U+001F1. Die letzte Gruppe ist die gemeine, denn ein echter Zeilenumbruch in einem String ist ein Steuerzeichen.
{
"bio": "line one
line two"
}
Sie bekommen Bad control character in string literal in JSON at position 20 (line 2 column 19), und Position 20 ist der Zeilenumbruch selbst, nicht das Zeichen danach. Der alte Rat, in der Zeile darüber nachzusehen, ist für diesen Fall nicht mehr nötig.
Die eigentliche Lehre liegt weiter oben. Bauen Sie JSON mit JSON.stringify oder dem Äquivalent Ihrer Sprache, nie durch Zusammenkleben von Strings in einem Template, denn genau so kommt ein nicht maskiertes Anführungszeichen überhaupt erst in einen Payload.
NaN, Infinity, undefined
JSON.stringify macht aus NaN und Infinity ein null. undefined wird als Objekteigenschaft weggelassen und als Array-Element zu null2, ein Unterschied, den man kennen sollte, bevor man nach einem fehlenden Array-Platz sucht.
Ein Payload mit literalem NaN kam also nicht aus JavaScript. Sehr oft kam er aus Python, dessen json-Modul standardmässig in beide Richtungen nicht strikt ist: dumps gibt NaN, Infinity und -Infinity aus, und loads akzeptiert sie beim Einlesen wieder3. Die Dokumentation sagt selbst, dass die Ergebnisse kein gültiges JSON sind. Genau diese Symmetrie sorgt dafür, dass es niemandem auffällt: Der Payload läuft innerhalb von Python sauber hin und zurück und fliegt in dem Moment auseinander, in dem ein Go- oder Java-Dienst ihn liest.
Reparieren Sie es beim Erzeuger mit json.dumps(obj, allow_nan=False), was schon beim Serialisieren einen Fehler wirft, statt etwas Unparsbares auszuliefern.
Ein Byte Order Mark
Manche Editoren und Exporte stellen U+FEFF voran. RFC 8259 verbietet, eines an übertragenes JSON anzuhängen, erlaubt einem Parser aber ausdrücklich, eines zu ignorieren statt zu scheitern1. Beide Verhalten sind konform, und Sie können sich nicht herleiten, welches Sie bekommen.
In der Praxis lehnen es sowohl JSON.parse als auch Pythons json.loads ab. Pythons Meldung ist die hilfreichere von beiden: Unexpected UTF-8 BOM (decode using utf-8-sig).
$ file config.json
config.json: Unicode text, UTF-8 (with BOM) text
$ head -c 3 config.json | xxd -p
efbbbf
Entfernen lässt es sich mit sed '1s/^\xEF\xBB\xBF//' config.json oder indem Sie im Editor als "UTF-8 ohne BOM" speichern.
Die Fehler, die nicht verraten, wo sie sind
Jetzt der Teil, der Zeit kostet, und der Grund, warum jeder von Stack Overflow kopierte Positions-Kratzer irgendwann bricht.
V8 erzeugt zwei Formen von Meldung, und nur eine enthält eine Position.
Die zweite Familie zitiert stattdessen einen Ausschnitt des Payloads zurück. Bei einem kurzen Payload ist das unproblematisch, weil der Ausschnitt das Ganze ist. Bei einem grossen ist es ein Fragment mit Auslassungspunkten, und das reicht immer noch, denn danach können Sie suchen.
function locate(text) {
try {
JSON.parse(text);
return null;
} catch (err) {
const pos = err.message.match(/position (\d+)/);
if (pos) return Number(pos[1]);
const quoted = err.message.match(/"(.+)" is not valid JSON$/s);
if (quoted) {
const at = text.indexOf(quoted[1].replace(/^\.\.\./, ''));
if (at >= 0) return at;
}
return null;
}
}
Das deckt beide Familien ab. Gegen einen Payload mit einem NaN-Literal an Position 107 getestet findet der Positions-Zweig nichts, und der Ausschnitt-Zweig landet auf 107.
Wenn Sie einen Offset haben, lassen Sie die Umgebung ausgeben, statt Zeichen von Hand zu zählen:
const at = locate(payload);
console.log(payload.slice(Math.max(0, at - 80), at + 80));
Eine frühere Fassung dieses Artikels empfahl stattdessen eine Bisektion: den Payload halbieren und jedes Präfix mit angeklebtem '}]'.repeat(10) erneut parsen. Das funktioniert nicht, und ich hätte es vor dem Veröffentlichen ausführen sollen. Fast kein Präfix plus zehn schliessende Klammern ist gültiges JSON, die Suche fällt also schon im ersten Schritt auf das erste Byte zusammen. Bei einem 4,6 KB grossen Testpayload mit einem echten Syntaxfehler an Index 4593 lieferte sie 1.
Bisektion ist ohnehin der falsche Reflex. Der Parser kennt den Offset bereits und gibt ihn heraus.
Wenn Sie lieber kein JavaScript schreiben
jq meldet die Position in der Form, mit der die meisten am besten arbeiten:
$ jq . config.json
jq: parse error: Expected another key-value pair at line 3, column 1
Zeile und Spalte schlagen einen Zeichen-Offset für alles, was Sie danach im Editor öffnen. Unser JSON-Formatter erledigt dasselbe im Browser-Tab und zeigt auf das Zeichen.
Gegen die Version diffen, die funktioniert hat
Wenn ein Payload gestern noch geparst wurde und heute scheitert, ist der Vergleich schneller als das Lesen von beidem. Schicken Sie beide Fassungen zuerst durch den Formatter, damit strukturelle Änderungen als Zeilenänderungen auftauchen und sich nicht in einer riesigen Zeile verstecken, und geben Sie sie dann in den Diff-Checker , der vollständig im Browser läuft. Letzteres zählt, wenn der Payload eine Produktionsantwort ist, die Sie nicht auf den Server eines Fremden kopieren sollten.
Auf Abbruch prüfen, bevor Sie auf Syntax prüfen
Kommt der Payload aus einer API und ist das letzte Zeichen weder } noch ], dann ist das kein Syntaxfehler. Der Body wurde abgeschnitten: eine Verbindung geschlossen, ein Puffer zu früh geleert, ein Timeout mitten in der Serialisierung. Der Text hört einfach auf, oft mitten in einem String, und Sie bekommen Unterminated string in JSON at position ....
Keine Parser-Option repariert das. Der Fehler liegt beim Erzeuger.
Fünf Minuten, kaputter Payload, los
- Die Erwartung in der Meldung lesen, dann von der Position aus nach links schauen.
- Das letzte Zeichen prüfen. Weder
}noch]heisst Abbruch, und hier sind Sie fertig. - Nach
,\s*[}\]]greppen. Nachgestellte Kommas bleiben Nummer eins. - Die ersten drei Bytes auf
ef bb bfprüfen. - Nach
NaN,Infinityundundefinedgreppen. Wenn Sie fündig werden, reden Sie mit dem, der die Datei erzeugt hat.
Das meiste kaputte JSON ist einer dieser Fälle. Wirklich seltsame Fehler, tiefe strukturelle Schäden oder gemischte Kodierungen, sind selten genug, um die volle halbe Stunde zu verdienen, wenn sie auftauchen.
Eine Sache, die man nicht tun sollte
Tools, die fehlerhaftes JSON "reparieren", indem sie es grosszügig umdeuten, raten, was Sie gemeint haben, und verdecken den Fehler beim Erzeuger, der Ihnen nächste Woche einen schlimmeren Payload schickt. Reparieren Sie die Quelle. Der Parser hatte recht.
Quellen
Jede Zahl in diesem Artikel lässt sich auf eine Quelle unten zurückführen. Behauptungen ohne Beleg wurden gestrichen, nicht abgeschwächt.
- PrimärquelleIETF
Abschnitt 7, dass Anführungszeichen, Backslash und die Steuerzeichen U+0000 bis U+001F innerhalb von Strings maskiert werden müssen, und Abschnitt 8.1, dass ein Parser ein Byte Order Mark ignorieren darf, statt es als Fehler zu behandeln.
- PrimärquelleMDN Web Docs
Dass Infinity und NaN als null serialisiert werden und dass undefined in einem Objekt weggelassen wird, in einem Array dagegen zu null wird.
- PrimärquellePython Software Foundation
Dass allow_nan auf True steht und damit NaN und Infinity ausgegeben werden, dass der Decoder diese Literale beim Einlesen akzeptiert, und die Aussage der Dokumentation selbst, dass die Ergebnisse kein gültiges JSON sind.
Themen
- JSON
- Debugging
- Parsers
- Regex
- Diff
In diesem Artikel erwähnte Tools
- JSON Dialect Detector - Tell whether a file is JSON, JSONC or JSON5 - and convert it to strict JSON.
- JSON Formatter - Format, validate and minify JSON with syntax highlighting.
- Regex Tester - Test regular expressions with live highlighting, matches and capture groups.
- Diff Checker - Compare code or text with line-by-line diff and unified output.
Neue Tools per E-Mail
Neue Tools und gelegentlich ein ausführlicher Artikel, etwa einmal im Monat. Kein Spam, deine Adresse bekommt niemand, Abmeldung mit einem Klick.