Mode Strict et Conformité à la RFC 8259 dans la Validation JSON
Par Aerisium Core · ·
La grammaire JSON définie dans la RFC 8259 est un sous-ensemble strict de la syntaxe des littéraux d’objet JavaScript. Les deux se ressemblent presque, mais les différences provoquent systématiquement des échecs en production : des données qui s’analysent sans erreur dans le navigateur d’un développeur cassent silencieusement les consommateurs en aval qui exécutent des analyseurs conformes. Comprendre exactement où les grammaires divergent est essentiel pour construire des pipelines de données fiables.
Littéraux d’Objet JavaScript face à la Grammaire JSON
La syntaxe des objets JavaScript a évolué à travers les spécifications ECMAScript, accumulant des commodités que JSON exclut délibérément. La RFC 8259 formalise une grammaire restreinte sans syntaxe optionnelle.
Les incompatibilités critiques sont :
-
Trailing commas : JavaScript permet
{ "a" : 1, "b" : 2, }. La grammaire JSON rejette toute virgule après la dernière paire clé-valeur. L’analyseur attend une virgule uniquement entre les paires : après la dernière paire, le jeton suivant doit être l’accolade fermante. -
Chaînes entre guillemets simples : JavaScript accepte
'key'et"key"indifféremment. La section 7 de la RFC 8259 impose que les chaînes soient délimitées exclusivement par des guillemets U+0022. Les guillemets simples (U+0027) n’ont aucun sens dans la grammaire lexicale JSON. -
Clés sans guillemets : JavaScript permet
{ key: "value" }oùkeyest traité comme un identifiant. La grammaire JSON exige que chaque clé soit une chaîne entre guillemets. -
Commentaires : Les objets JavaScript peuvent contenir des commentaires
//et/* */. La grammaire JSON n’a pas de production de commentaire. La barre oblique (/) n’apparaît que dans les séquences d’échappement. -
Undefined et NaN :
JSON.stringifyde JavaScript supprime silencieusement les valeursundefinedet convertitNaNennull. Un analyseur qui accepte ces valeurs en entrée produit des données qui ne survivent pas à un aller-retour viaJSON.parse.
Trailing Commas dans les Pipelines de Production
L’acceptation des trailing commas est l’échec de validation JSON le plus courant dans les systèmes réels. Considérons un fichier de configuration consommé par un processeur de flux basé sur Java :
{
"queue_batch_size": 1000,
"retry_policy": "exponential_backoff",
"dead_letter_topic": "dlq.primary",
}
La trailing comma après "dead_letter_topic" fait que tout analyseur conforme à la RFC 8259, comme com.google.gson.stream.JsonReader ou com.fasterxml.jackson.core.JsonParser, lève une exception d’analyse. Le processeur de flux plante au démarrage. Le déploiement échoue. L’incident coûte des heures d’ingénierie à diagnostiquer car le même JSON s’affiche sans erreur dans tous les formateurs basés sur navigateur qui tolèrent les trailing commas.
La cause profonde est un décalage entre les outils de développement et l’analyse en production. Les développeurs travaillant dans le navigateur ne voient jamais l’erreur car JSON.parse() de V8 est strictement conforme à la RFC depuis ES5 et rejette également les trailing commas. La déconnexion se produit lorsque des outils intermédiaires — éditeurs, formateurs ou scripts personnalisés — acceptent silencieusement l’entrée malformée avant que les données n’atteignent l’analyseur de production. Lorsque l’analyseur strict la rejette, l’artefact a déjà été déployé.
Validation en Streaming face à la Construction d’AST
Valider un document JSON par rapport à la RFC 8259 peut s’effectuer à deux niveaux, chacun avec une complexité informatique distincte.
Un analyseur lexical en streaming s’exécute en temps O(n) et mémoire O(1). L’analyseur classe chaque caractère séquentiellement : les espaces blancs sont ignorés, les jetons structurels ({, }, [, ], :, ,) sont validés aux positions attendues, les chaînes sont scrutées pour les caractères de contrôle non échappés, et les nombres sont vérifiés par rapport à la grammaire numérique (-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?). L’analyseur lexical rejette l’entrée malformée au premier caractère invalide et se termine immédiatement.
La construction complète de l’AST nécessite un espace O(n + m), où m est le nombre de nœuds alloués. Un document profondément imbriqué comme {"a":{"a":{"a":{"a":null}}}} alloue un nouveau descripteur d’objet dans le heap de V8 pour chaque niveau d’imbrication. Un tableau plat de 10 Mo de primitives produit un petit AST : un nœud tableau plus N nœuds primitifs. Un document de 10 Mo d’objets profondément imbriqués avec des noms de clé longs peut nécessiter 8 à 12 fois la taille brute en octets dans la mémoire du heap.
// Validateur en streaming O(n) — pas d'AST, mémoire O(1)
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) { /* fin */ }
else return false;
break;
}
}
return depth === 0;
}
Un validateur en streaming atteint un temps O(n) et une mémoire O(1), ce qui le rend adapté à la validation de payloads de plusieurs gigaoctets sans épuisement du heap. La contrepartie est le rapport d’erreurs : un validateur en streaming ne peut pas signaler le contexte structurel précis d’une erreur (par exemple, “virgule attendue à la ligne 42 372, colonne 15”) sans maintenir des métadonnées de position, ce qui pousse la complexité vers O(log n) pour un suivi de ligne.
Prévention des Défaillances Silencieuses dans les Pipelines CI/CD
La classe la plus dangereuse d’échec de validation JSON se produit lorsqu’un pipeline CI/CD utilise un analyseur permissif lors de la compilation et un analyseur strict à l’exécution.
Considérons un pipeline de déploiement où un fichier de configuration TypeScript est traité par ts-node pendant la compilation, qui évalue le fichier en tant que JavaScript, acceptant les trailing commas et les guillemets simples. L’artefact de sortie est écrit sous forme de fichier .json. Le service en production lit l’artefact en utilisant encoding/json de Go, qui est conforme à la RFC 8259.
L’artefact passe la validation de compilation car ts-node n’invoque jamais JSON.parse() sur la sortie. Les valeurs de configuration sont correctes. La compilation réussit. L’artefact est déployé. La défaillance se manifeste seulement lorsque le service de production tente de revalider ou de transformer l’artefact, ou lorsqu’un système externe le consomme. À ce stade, l’artefact a déjà été déployé et le retour en arrière nécessite un cycle de déploiement complet.
La solution est d’appliquer la validation RFC 8259 au point le plus précoce possible du pipeline : un hook de pré-commit ou une étape de compilation, en utilisant un analyseur qui correspond au comportement du runtime de production. Toute divergence entre l’analyse au moment de la compilation et celle à l’exécution est une dette technique qui se matérialisera éventuellement sous forme d’incident de production. Le validateur fiable le plus simple est JSON.parse() : il est intégré dans tous les runtimes modernes, strictement conforme à la RFC, et garanti de correspondre à tout analyseur fourni avec votre environnement de production.