Strikte Modus en RFC 8259-naleving bij JSON-validatie

Door Aerisium Core · ·

De JSON-grammatica gedefinieerd in RFC 8259 is een strikte subset van de JavaScript-objectliteral-syntaxis. Beide zien er bijna identiek uit, maar de verschillen veroorzaken consistent productiefouten: data die zonder fout wordt geparseerd in de browser van een ontwikkelaar breekt stilletjes stroomafwaartse consumenten die conforme parsers draaien. Begrijpen waar de grammatica’s precies uiteenlopen is cruciaal voor het bouwen van betrouwbare datapijplijnen.

JavaScript-objectliterals versus de JSON-grammatica

De objectsyntaxis van JavaScript evolueerde via ECMAScript-specificaties en verzamelde gemakken die JSON bewust uitsluit. RFC 8259 formaliseert een beperkte grammatica zonder optionele syntaxis.

De kritieke incompatibiliteiten zijn:

  • Trailing commas: JavaScript staat { "a": 1, "b": 2, } toe. De JSON-grammatica wijst elke komma na het laatste sleutel-waardepaar af. De parser verwacht alleen een komma tussen paren: na het laatste paar moet het volgende token de sluitende accolade zijn.

  • Strings met enkele aanhalingstekens: JavaScript accepteert 'key' en "key" door elkaar. RFC 8259 sectie 7 schrijft voor dat strings uitsluitend worden begrensd door U+0022-aanhalingstekens. Enkele aanhalingstekens (U+0027) hebben geen betekenis in de JSON-lexicale grammatica.

  • Sleutels zonder aanhalingstekens: JavaScript staat { key: "value" } toe waarbij key als identifier wordt behandeld. De JSON-grammatica vereist dat elke sleutel een string tussen aanhalingstekens is.

  • Commentaar: JavaScript-objecten kunnen //- en /* */-commentaar bevatten. De JSON-grammatica heeft geen commentaarproductie. De schuine streep (/) verschijnt alleen in escape-sequenties.

  • Undefined en NaN: JSON.stringify van JavaScript verwijdert stilletjes undefined-waarden en converteert NaN naar null. Een parser die deze waarden als invoer accepteert, produceert data die een round-trip door JSON.parse niet overleeft.

Trailing Commas in Productiepijplijnen

Acceptatie van trailing commas is de meest voorkomende JSON-validatiefout in echte systemen. Beschouw een configuratiebestand dat wordt verwerkt door een Java-gebaseerde streamprocessor:


{
    "queue_batch_size": 1000,
    "retry_policy": "exponential_backoff",
    "dead_letter_topic": "dlq.primary",

}

De trailing comma na "dead_letter_topic" zorgt dat elke RFC 8259-conforme parser, zoals com.google.gson.stream.JsonReader of com.fasterxml.jackson.core.JsonParser, een parse-uitzondering gooit. De streamprocessor crasht bij het opstarten. De implementatie mislukt. Het incident kost engineering-uren om te diagnosticeren omdat dezelfde JSON foutloos wordt weergegeven in elke browser-gebaseerde formatter die trailing commas tolereert.

De oorzaak is een mismatch tussen ontwikkelingstools en productie-parsing. Ontwikkelaars in de browser zien de fout nooit omdat JSON.parse() van V8 sinds ES5 strikt RFC-conform is en ook trailing commas afwijst. De ontkoppeling ontstaat wanneer tussenliggende tools — editors, formatters of aangepaste scripts — stilletjes de foutieve invoer accepteren voordat de data de productie-parser bereikt. Tegen de tijd dat de strikte parser deze afwijst, is het artefact al geïmplementeerd.

Streaming-validatie versus AST-constructie

Het valideren van een JSON-document tegen RFC 8259 kan op twee niveaus worden uitgevoerd, elk met een aparte computationele complexiteit.

Een streaming lexer draait in O(n)-tijd en O(1)-geheugen. De parser classificeert elk teken sequentieel: witruimte wordt weggegooid, structurele tokens ({, }, [, ], :, ,) worden gevalideerd op verwachte posities, strings worden gescand op niet-geescape controletekens, en getallen worden gecontroleerd tegen de numerieke grammatica (-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?). De lexer wijst ongeldige invoer af bij het eerste ongeldige teken en stopt onmiddellijk.

Volledige AST-constructie vereist O(n + m)-ruimte, waarbij m het aantal toegewezen knooppunten is. Een diep genest document zoals {"a":{"a":{"a":{"a":null}}}} wijst een nieuwe objectdescriptor toe in V8’s heap voor elk nestniveau. Een vlak 10-MB-array van primitieven produceert een kleine AST: één array-knooppunt plus N primitieve knooppunten. Een 10-MB-document van diep geneste objecten met lange sleutelnamen kan 8–12 keer de ruwe bytegrootte aan heap-geheugen vereisen.


// O(n) streaming validator — geen AST, O(1) geheugen

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) { /* einde */ }
        else return false;
        break;
    }
  }
  return depth === 0;

}

Een streaming validator bereikt O(n)-tijd en O(1)-geheugen, waardoor deze geschikt is voor het valideren van payloads van meerdere gigabytes zonder heap-uitputting. De afweging is foutrapportage: een streaming validator kan de precieze structurele context van een fout niet rapporteren (bijvoorbeeld “verwacht een komma op regel 42.372, kolom 15”) zonder positiemetadata bij te houden, wat de complexiteit voor een regeltracker richting O(log n) drijft.

Voorkomen van stille CI/CD-fouten

De gevaarlijkste klasse van JSON-validatiefouten treedt op wanneer een CI/CD-pijplijn een toegeeflijke parser gebruikt tijdens de build en een strikte parser tijdens runtime.

Beschouw een implementatiepijplijn waarbij een TypeScript-configuratiebestand tijdens de build wordt verwerkt door ts-node, dat het bestand als JavaScript evalueert en trailing commas en enkele aanhalingstekens accepteert. Het uitvoerartefact wordt geschreven als een .json-bestand. De productieservice leest het artefact met Go’s encoding/json, dat RFC 8259-conform is.

Het artefact doorstaat de build-validatie omdat ts-node nooit JSON.parse() op de uitvoer aanroept. De configuratiewaarden zijn correct. De build slaagt. Het artefact wordt geïmplementeerd. De fout manifesteert zich pas wanneer de productieservice probeert het artefact te hervalideren of transformeren, of wanneer een extern systeem het consumeert. Tegen die tijd is het artefact al geïmplementeerd en vereist de terugdraaiing een volledige implementatiecyclus.

De oplossing is om RFC 8259-validatie op het vroegst mogelijke punt in de pijplijn af te dwingen: een pre-commit hook of build-stap, met een parser die overeenkomt met het gedrag van de productieruntime. Elke afwijking tussen build-time en runtime-parsing is technische schuld die zich uiteindelijk zal manifesteren als een productie-incident. De eenvoudigste betrouwbare validator is JSON.parse(): het zit ingebouwd in elke moderne runtime, is strikt RFC-conform en gegarandeerd identiek aan elke parser die met uw productieomgeving wordt meegeleverd.

JSONClear. Crafted without compromise by Aerisium.