Aller au contenu
jsonbeautifiers
Français

JSON n’a pas de commentaires, et c’était délibéré

Les commentaires ont été retirés de JSON pour protéger l’interopérabilité, et cette décision a été payée par chaque fichier de configuration depuis.

Chaque affirmation de cette page est soit mesurée, soit sourcée. Quand elle n’est ni l’une ni l’autre, la page le dit.

Vous ajoutez une ligne à un fichier de configuration pour expliquer pourquoi un délai d’attente est de 45 secondes et non de 30, le déploiement échoue, et le message ne vous apprend rien :

JSON.parse('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
// Expected property name or '}' in JSON at position 4 (line 2 column 3)

L’analyseur a vu une barre oblique là où il attendait une clé, et il a abandonné. Python n’est pas plus explicite sur la cause :

json.loads('{\n  // 45s: upstream p99 is 38s\n  "timeout": 45\n}')
# JSONDecodeError: Expecting property name enclosed in double quotes: line 2 column 3 (char 4)

Aucun des deux messages ne mentionne les commentaires, parce que du point de vue de la grammaire il n’y a rien à mentionner. La RFC 8259 définit exactement quatre caractères autorisés entre les jetons : espace, tabulation, retour chariot et saut de ligne. Tout le reste fait partie d’une valeur ou constitue une erreur de syntaxe.

Pourquoi ils ont été retirés

Les commentaires figuraient dans les premières versions de JSON, et Douglas Crockford les a retirés. Sa raison déclarée est la partie intéressante : les gens ne s’en servaient pas pour de la prose, ils s’en servaient pour transporter des directives d’analyse. Une indication d’encodage, un pointeur vers un schéma, écrit dans un commentaire, qu’un consommateur particulier lirait et exécuterait. À ce stade, le commentaire n’est plus un commentaire. C’est un second canal de données non documenté, transporté dans un format dont l’argument de vente était précisément que n’importe quel analyseur, n’importe où, lirait les mêmes valeurs à partir des mêmes octets.

La solution de remplacement suggérée par Crockford lui-même consistait à passer votre fichier commenté dans un minificateur avant de le donner à un analyseur. Cela reste la bonne forme de réponse, et l’essentiel de cet article porte sur la manière de le faire correctement.

La décision se défendait pour ce que JSON était à ses débuts : un format de transport servant à déplacer une valeur entre deux programmes qui s’étaient déjà entendus sur son sens. Personne ne commente un paquet réseau.

Pourquoi ça fait mal quand même

JSON n’est pas resté un format de transport. Il est devenu le langage de configuration par défaut de toute la chaîne d’outils, et la configuration est précisément le cas où le raisonnement derrière une valeur importe plus que la valeur. Un retries: 0 sans explication finit « corrigé » par la personne suivante, en toute bonne foi. Un retries: 0 surmonté de // volontaire, cet endpoint n’est pas idempotent, non.

Chaque écosystème qui a adopté JSON pour la configuration s’est donc bricolé son propre correctif par-dessus, et ils ne sont pas compatibles entre eux.

Les cinq options

Une clé _comment

{
  "_comment": "45s parce que le p99 en amont est de 38s",
  "timeout": 45
}

C’est du JSON strict, ça s’analyse partout et ça ne demande aucun outillage. Les problèmes sont réels, cependant. Votre schéma doit désormais l’autoriser, sinon votre validateur le rejette. C’est une donnée : elle part chez les clients, atterrit dans les journaux, et apparaît dans les diffs comme un changement de valeur plutôt que comme un changement de commentaire. Et vous en avez exactement une par objet : la RFC 8259 dit que les clés DEVRAIENT être uniques et laisse les doublons indéfinis, JavaScript comme Python retenant la dernière, si bien qu’un second _comment au même niveau écrase le premier en silence. Les gens contournent avec _comment1, _comment2, et c’est le moment où l’approche cesse d’en valoir la peine.

Utilisez-la pour une note d’en-tête en haut d’un fichier. Ne l’utilisez pas pour annoter ligne par ligne.

JSONC

JSONC, c’est JSON plus deux choses : les commentaires // et /* */, et les virgules finales. Rien d’autre. C’est ce que VS Code utilise pour ses propres settings.json et keybindings.json, et ce que TypeScript accepte dans tsconfig.json.

{
  // le p99 en amont est de 38s
  "timeout": 45,
  "retries": 0, // cet endpoint n’est pas idempotent
}

Autant être franc sur son statut : il n’existe aucune spécification indépendante de JSONC. Pas de RFC, pas de numéro de version, pas de suite de conformité. C’est une convention adossée à une implémentation en forme d’éditeur, et les dialectes divergent sur les bords (une virgule finale après le dernier élément d’un tableau est-elle acceptée, les commentaires survivent-ils à un aller-retour). C’est l’option la plus sûre quand votre consommateur est déjà un outil qui la prend en charge, et un mauvais choix pour tout ce que vous remettez à un tiers.

JSON5

JSON5 est une vraie spécification, avec un historique de versions, et elle va nettement plus loin que JSONC :

  • Clés d’objet sans guillemets, quand la clé est un identifiant ES5 valide
  • Chaînes en guillemets simples
  • Virgules finales dans les objets et les tableaux
  • Commentaires de ligne et de bloc
  • Nombres hexadécimaux
  • Points décimaux en tête et en queue, donc .5 et 5. sont des nombres
  • Infinity, -Infinity et NaN

C’est ce dernier point qu’il faut peser sérieusement. NaN et les infinis n’ont aucune représentation en JSON, donc un document JSON5 qui les utilise ne peut pas être converti en JSON sans une décision avec perte sur ce qu’on met à la place. Les autres extensions sont cosmétiques et survivent bien à une conversion. Utilisez JSON5 quand l’auteur principal du fichier est un humain et qu’une extension .json5 est acceptable ; ne l’utilisez pas comme format d’API.

Arrêtez d’utiliser JSON

Si le fichier est une configuration que vous maîtrisez de bout en bout, et que rien d’externe ne le consomme, le format est un choix libre et JSON n’est pas manifestement le meilleur. YAML et TOML ont des commentaires de première classe. Chacun a ses propres coûts, et le comparatif mérite une lecture avant de vous engager, car YAML en particulier vous offrira le problème norvégien : sous la sémantique de YAML 1.1, qu’implémentent PyYAML et le Psych de Ruby, un no sans guillemets s’analyse comme le booléen false.

Retirez-les à la compilation

Gardez le fichier annoté comme source de vérité, retirez les commentaires dans la CI, publiez du JSON strict. C’est la suggestion de Crockford et cela satisfait tout le monde : vos éditeurs et vos relecteurs voient les commentaires, votre analyseur à l’exécution voit un document conforme à la RFC 8259, et aucun consommateur n’a besoin de savoir que l’un ou l’autre format existe.

Retirer les commentaires sans casser les URL

L’implémentation évidente est une expression régulière, et l’expression régulière évidente est fausse :

// Ne faites pas ça.
text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');

Lancez-la sur ceci et regardez-la détruire une valeur :

{
  "endpoint": "https://api.example.com/v2/orders", // production
  "note": "consultez /* le runbook */ avant de changer ça"
}

La première règle trouve // dans https:// et supprime le reste de la ligne, guillemet fermant et virgule compris. La seconde trouve un commentaire de bloc à l’intérieur d’une chaîne. Vous vous retrouvez avec une chaîne non terminée et une erreur qui pointe vers un endroit sans aucun rapport. Une regex ne peut pas faire ce travail parce qu’elle ne peut pas savoir si une barre oblique est à l’intérieur d’une chaîne, et le contexte de chaîne en JSON dépend du comptage des échappements.

Il vous faut un scanner qui suit exactement un état :

function stripJsonComments(text) {
  let out = '';
  let inString = false;
  let inLine = false;
  let inBlock = false;

  for (let i = 0; i < text.length; i++) {
    const c = text[i];
    const next = text[i + 1];

    if (inLine) {
      if (c === '\n') { inLine = false; out += c; }
      continue;
    }
    if (inBlock) {
      // conserve les sauts de ligne pour que les numéros de ligne restent justes
      if (c === '*' && next === '/') { inBlock = false; i++; }
      else if (c === '\n') { out += c; }
      continue;
    }
    if (inString) {
      out += c;
      if (c === '\\') { out += next; i++; continue; }  // échappement, consomme les deux
      if (c === '"') inString = false;
      continue;
    }
    if (c === '"') { inString = true; out += c; continue; }
    if (c === '/' && next === '/') { inLine = true; i++; continue; }
    if (c === '/' && next === '*') { inBlock = true; i++; continue; }
    out += c;
  }
  return out;
}

La branche d’échappement est celle que les gens oublient. Sans elle, le guillemet échappé de "il a dit \"va sur https://example.com\" aujourd’hui" se lit comme la fin de la chaîne, donc le // qui suit est pris pour le début d’un commentaire et le reste de la ligne disparaît.

Remarquez aussi ce que cela ne fait pas. Retirer les commentaires laisse les virgules finales, et celles-ci échouent de leur côté avec leur propre erreur : V8 signale Expected double-quoted property name in JSON at position 7 (line 1 column 8) pour {"a":1,}, et le tout autre Unexpected token ']', "[1,2,]" is not valid JSON pour [1,2,]. Une conversion de JSONC vers JSON doit traiter les deux.

Si vous préférez ne pas embarquer le scanner, collez le fichier dans Réparer du JSON, qui retire commentaires et virgules finales en une seule passe et vous rend du JSON strict, puis confirmez le résultat avec le validateur. Les deux s’exécutent entièrement dans votre navigateur, ce qui compte quand le fichier que vous réparez est une configuration de production contenant des identifiants. Les autres pages d’erreurs couvrent la marche à suivre quand la panne s’avère n’être pas du tout un commentaire.