Strenger Modus und RFC-8259-Konformität bei der JSON-Validierung
Von Aerisium Core · ·
Die in RFC 8259 definierte JSON-Grammatik ist eine strenge Teilmenge der JavaScript-Objektliteral-Syntax. Beide sehen nahezu identisch aus, aber die Unterschiede verursachen konsistent Produktionsausfälle: Daten, die im Browser eines Entwicklers fehlerfrei geparst werden, zerstören stillschweigend nachgelagerte Verbraucher, die konforme Parser ausführen. Zu verstehen, wo die Grammatiken auseinandergehen, ist entscheidend für den Aufbau zuverlässiger Datenpipelines.
JavaScript-Objektliterale versus die JSON-Grammatik
Die Objektsyntax von JavaScript entwickelte sich durch ECMAScript-Spezifikationen und sammelte Annehmlichkeiten an, die JSON bewusst ausschließt. RFC 8259 formalisiert eine eingeschränkte Grammatik ohne optionale Syntax.
Die kritischen Inkompatibilitäten sind:
-
Trailing Commas: JavaScript erlaubt
{ "a": 1, "b": 2, }. Die JSON-Grammatik weist jedes Komma nach dem letzten Schlüssel-Wert-Paar zurück. Der Parser erwartet ein Komma nur zwischen Paaren: nach dem letzten Paar muss das nächste Token die schließende geschweifte Klammer sein. -
Strings mit einfachen Anführungszeichen: JavaScript akzeptiert
'key'und"key"austauschbar. RFC 8259 Abschnitt 7 schreibt vor, dass Strings ausschließlich durch U+0022-Anführungszeichen begrenzt werden. Einfache Anführungszeichen (U+0027) haben in der JSON-Lexikalgrammatik keine Bedeutung. -
Schlüssel ohne Anführungszeichen: JavaScript erlaubt
{ key: "value" }, wobeikeyals Bezeichner behandelt wird. Die JSON-Grammatik verlangt, dass jeder Schlüssel ein in Anführungszeichen gesetzter String ist. -
Kommentare: JavaScript-Objekte können
//- und/* */-Kommentare enthalten. Die JSON-Grammatik hat keine Kommentarproduktion. Der Schrägstrich (/) erscheint nur in Escape-Sequenzen. -
Undefined und NaN:
JSON.stringifyvon JavaScript verwirft stillschweigendundefined-Werte und wandeltNaNinnullum. Ein Parser, der diese Werte als Eingabe akzeptiert, erzeugt Daten, die einen Roundtrip durchJSON.parsenicht überstehen.
Trailing Commas in Produktionspipelines
Die Akzeptanz von Trailing Commas ist der häufigste JSON-Validierungsfehler in realen Systemen. Betrachten wir eine Konfigurationsdatei, die von einem Java-basierten Stream-Prozessor verarbeitet wird:
{
"queue_batch_size": 1000,
"retry_policy": "exponential_backoff",
"dead_letter_topic": "dlq.primary",
}
Das Trailing Comma nach "dead_letter_topic" führt dazu, dass jeder RFC-8259-konforme Parser, wie com.google.gson.stream.JsonReader oder com.fasterxml.jackson.core.JsonParser, eine Parse-Ausnahme auslöst. Der Stream-Prozessor stürzt beim Start ab. Die Bereitstellung schlägt fehl. Der Vorfall kostet Ingenieurstunden für die Diagnose, weil dasselbe JSON in jedem browserbasierten Formatierer, der Trailing Commas toleriert, fehlerfrei dargestellt wird.
Die Grundursache ist eine Diskrepanz zwischen Entwicklungswerkzeugen und Produktions-Parsing. Browserentwickler sehen den Fehler nie, da JSON.parse() von V8 seit ES5 streng RFC-konform ist und ebenfalls Trailing Commas ablehnt. Die Trennung tritt auf, wenn Zwischenwerkzeuge — Editoren, Formatierer oder benutzerdefinierte Skripte — stillschweigend die fehlerhafte Eingabe akzeptieren, bevor die Daten den Produktionsparser erreichen. Wenn der strenge Parser sie zurückweist, ist das Artefakt bereits ausgeliefert.
Streaming-Validierung versus AST-Konstruktion
Die Validierung eines JSON-Dokuments gegen RFC 8259 kann auf zwei Ebenen erfolgen, jede mit unterschiedlicher Rechenkomplexität.
Ein Streaming-Lexer läuft in O(n)-Zeit und O(1)-Speicher. Der Parser klassifiziert jedes Zeichen sequentiell: Leerraum wird verworfen, strukturelle Tokens ({, }, [, ], :, ,) werden an erwarteten Positionen validiert, Strings werden auf nicht escapierte Steuerzeichen geprüft, und Zahlen werden gegen die numerische Grammatik (-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?) verifiziert. Der Lexer weist fehlerhafte Eingabe beim ersten ungültigen Zeichen zurück und terminiert sofort.
Die vollständige AST-Konstruktion erfordert O(n + m) Speicher, wobei m die Anzahl der zugewiesenen Knoten ist. Ein tief verschachteltes Dokument wie {"a":{"a":{"a":{"a":null}}}} weist für jede Verschachtelungsebene einen neuen Objekt-Deskriptor im V8-Heap zu. Ein flaches 10-MB-Array von Primitiven erzeugt einen kleinen AST: einen Array-Knoten plus N primitive Knoten. Ein 10-MB-Dokument mit tief verschachtelten Objekten und langen Schlüsselnamen kann das 8- bis 12-fache der rohen Bytegröße im Heap-Speicher benötigen.
// O(n) Streaming-Validator — kein AST, O(1) Speicher
function validateJson(input) {
let i = 0;
let depth = 0;
let state = 'VALUE';
while (i < input.length) {
const ch = input[i];
switch (state) {
case 'VALUE':
if (ch === '{' || ch === '[') { depth++; state = ch === '{' ? 'KEY' : 'VALUE'; i++; }
else if (ch === '"') { state = 'STRING_END'; i++; }
else if (ch === 't') { if (input.slice(i, i + 4) === 'true') { i += 4; state = 'SEPARATOR'; } else return false; }
else if (ch === 'f') { if (input.slice(i, i + 5) === 'false') { i += 5; state = 'SEPARATOR'; } else return false; }
else if (ch === 'n') { if (input.slice(i, i + 4) === 'null') { i += 4; state = 'SEPARATOR'; } else return false; }
else if (ch === '-' || (ch >= '0' && ch <= '9')) { state = 'NUMBER'; i++; }
else if (ch === ' ') { i++; }
else return false;
break;
case 'KEY':
if (ch === '"') { state = 'KEY_END'; i++; }
else if (ch === '}') { depth--; state = 'SEPARATOR'; i++; }
else if (ch === ' ') { i++; }
else return false;
break;
case 'KEY_END':
if (ch === '"') { state = 'COLON'; i++; }
else if (ch === '\\') { i += 2; }
else if (ch >= ' ') { i++; }
else return false;
break;
case 'COLON':
if (ch === ':') { state = 'VALUE'; i++; }
else if (ch === ' ') { i++; }
else return false;
break;
case 'STRING_END':
if (ch === '"') { state = 'SEPARATOR'; i++; }
else if (ch === '\\') { i += 2; }
else if (ch >= ' ') { i++; }
else return false;
break;
case 'NUMBER':
if (ch >= '0' && ch <= '9') { i++; }
else if (ch === '.' || ch === 'e' || ch === 'E' || ch === '-' || ch === '+') { i++; }
else if (ch === ' ' || ch === ',' || ch === '}' || ch === ']') { state = ch === ',' ? 'VALUE' : 'SEPARATOR'; }
else return false;
break;
case 'SEPARATOR':
if (ch === ',') { state = 'VALUE'; i++; }
else if (ch === '}' || ch === ']') { depth--; i++; }
else if (ch === ' ') { i++; }
else if (i >= input.length) { /* Ende */ }
else return false;
break;
}
}
return depth === 0;
}
Ein Streaming-Validator erreicht O(n)-Zeit und O(1)-Speicher, was ihn für die Validierung von Multi-Gigabyte-Payloads ohne Heap-Erschöpfung geeignet macht. Der Nachteil ist die Fehlerberichterstattung: Ein Streaming-Validator kann den genauen strukturellen Kontext eines Fehlers (z. B. “Komma erwartet in Zeile 42.372, Spalte 15”) nicht melden, ohne Positionsmetadaten zu führen, was die Komplexität für einen Zeilenverfolger in Richtung O(log n) treibt.
Vermeidung stiller CI/CD-Fehler
Die gefährlichste Klasse von JSON-Validierungsfehlern tritt auf, wenn eine CI/CD-Pipeline während des Builds einen nachgiebigen Parser und zur Laufzeit einen strengen Parser verwendet.
Betrachten wir eine Bereitstellungspipeline, bei der eine TypeScript-Konfigurationsdatei während des Builds von ts-node verarbeitet wird. Dies wertet die Datei als JavaScript aus und akzeptiert dabei Trailing Commas und einfache Anführungszeichen. Das Ausgabeartefakt wird als .json-Datei geschrieben. Der Produktionsdienst liest das Artefakt mit Go’s encoding/json, das RFC-8259-konform ist.
Das Artefakt besteht die Build-Validierung, da ts-node niemals JSON.parse() auf der Ausgabe aufruft. Die Konfigurationswerte sind korrekt. Der Build ist erfolgreich. Das Artefakt wird bereitgestellt. Der Fehler manifestiert sich erst, wenn der Produktionsdienst versucht, das Artefakt erneut zu validieren oder zu transformieren, oder wenn ein externes System es konsumiert. Zu diesem Zeitpunkt ist das Artefakt bereits ausgeliefert und der Rollback erfordert einen vollständigen Bereitstellungszyklus.
Die Lösung besteht darin, die RFC-8259-Validierung am frühestmöglichen Punkt der Pipeline durchzusetzen: einem Pre-Commit-Hook oder Build-Schritt, der einen Parser verwendet, der dem Verhalten der Produktionslaufzeit entspricht. Jede Abweichung zwischen Build-Zeit- und Laufzeit-Parsing ist technische Schuld, die sich irgendwann als Produktionsvorfall materialisiert. Der einfachste zuverlässige Validator ist JSON.parse(): Er ist in jede moderne Laufzeit eingebaut, streng RFC-konform und garantiert mit jedem Parser identisch, der mit Ihrer Produktionsumgebung ausgeliefert wird.