JSON, YAML ou TOML : lequel prendre
Ces trois formats diffèrent moins par ce qu’ils savent exprimer que par leur façon d’échouer, et c’est sur les échecs que vous passerez votre temps.
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.
Un pipeline de déploiement lit une liste de pays dans un fichier YAML. Quelqu’un ajoute la Norvège avec son code ISO, NO, et le pipeline se met à ignorer ce marché sans la moindre erreur nulle part. La valeur arrivée à l’application était le booléen false.
Voilà le genre de chose qui devrait décider du format que vous choisissez, pas un tableau « prend en charge les commentaires : oui/non ». Les trois formats savent contenir une table de chaînes vers des valeurs. Ce qui les sépare, c’est ce qu’ils vous font quand personne ne regarde.
JSON : ennuyeux, et c’est précisément le but
JSON est un format de transport. Il a six types, quatre caractères blancs autorisés (espace, tabulation, retour chariot, saut de ligne, selon la RFC 8259), pas de commentaires, pas de virgules finales, pas de type date, et exactement un type numérique que tout lecteur est libre d’interpréter comme un float64. Il est sous-spécifié à deux endroits qui comptent, en particulier les clés dupliquées, où la RFC dit que les clés DEVRAIENT être uniques puis laisse le comportement indéfini. JavaScript et Python retiennent tous deux la dernière.
Ses vertus sont entièrement non techniques. Chaque langage livre un analyseur dans sa bibliothèque standard. Chaque client HTTP sait quoi en faire. Il n’y a pour ainsi dire pas de décalage de version : un document JSON écrit en 2008 s’analyse aujourd’hui, à l’identique, partout. Quand vous sérialisez pour un saut réseau, une ligne de journal, une file de messages ou un cache, aucune des fonctionnalités destinées aux humains des deux autres formats ne vous apporte quoi que ce soit, et l’universalité vous apporte beaucoup.
Les modes de défaillance sont bien balisés et portent surtout sur les nombres. Number.MAX_SAFE_INTEGER vaut 9007199254740991, et les identifiants au-dessus sont réécrits en silence, ce qui fait son propre article. Les dates sont des chaînes par convention et rien n’impose la convention, ce qui fait aussi son propre article. Aucun des deux n’est une raison de choisir un autre format pour le transport. Ce sont des raisons d’être prudent.
YAML : de vraies ergonomies, une vraie facture
On ne choisit pas YAML parce qu’il est élégant. On le choisit parce qu’un manifeste Kubernetes ou un pipeline de CI est une chose qu’un humain édite à la main tous les jours, et que JSON est réellement pénible à éditer à la main : pas de commentaires, guillemets obligatoires, et une virgule manquante quatre cents lignes plus haut. YAML vous donne des commentaires, des chaînes multilignes lisibles et aucun bruit de ponctuation. Cela vaut quelque chose.
Voici ce que vous payez.
Le problème norvégien
YAML 1.1 résout no, yes, on, off, y et n non guillemetés comme des booléens. Le schéma core de YAML 1.2 ne le fait pas et les laisse en chaînes. Même document, même clé, deux réponses :
a: no
Sous la résolution core de YAML 1.2, cette valeur est la chaîne "no". Sous les règles 1.1, c’est le booléen false. Ce que vous obtenez dépend de votre bibliothèque, pas de votre fichier : PyYAML et le Psych de Ruby résolvent selon 1.1, tandis que js-yaml suit 1.2. Le yaml.v3 de Go se place entre les deux : il résout no comme une chaîne sauf si le champ cible est un bool typé, auquel cas il accepte encore l’orthographe 1.1. Un service Python et un service Node qui lisent le même fichier de configuration sont en désaccord sur la valeur, et aucun des deux ne journalise quoi que ce soit.
Le remède est de guillemeter toute chaîne susceptible d’être prise pour autre chose. Codes pays, numéros de version (1.10 est un flottant, "1.10" non), tout ce qui commence par un zéro, et toute valeur fournie par un utilisateur. Si vous générez du YAML par programme, faites en sorte que l’émetteur guillemette de façon défensive plutôt que de compter sur votre relecture.
L’espace est de la syntaxe et les tabulations sont interdites
L’indentation porte la structure, donc une ligne mal alignée donne un autre document plutôt qu’une erreur. Pire, la spécification YAML interdit purement et simplement les tabulations pour l’indentation. Un éditeur configuré pour insérer une tabulation produit un fichier qui échoue à l’analyse avec un message portant sur un caractère invisible dans votre terminal. Réglez votre éditeur par type de fichier et cessez d’y penser.
Les ancres s’étalent à la sortie
Les ancres et alias permettent de définir un bloc une fois et de le réutiliser :
defaults: &defaults
timeout: 30
retries: 3
staging:
<<: *defaults
host: stage.internal
C’est la fonctionnalité qui vend YAML à qui maintient quarante définitions de services presque identiques. C’est aussi une fonctionnalité que le modèle de données ne possède pas. Convertissez ce fichier en JSON et la clé de fusion est résolue, l’alias est étalé, et defaults apparaît en entier dans staging. Refaites le chemin vers YAML et vous obtenez deux copies littérales. Rien n’est faux, à proprement parler, mais ce que vous mainteniez a disparu. Un fichier YAML qui s’appuie sur les ancres n’est pas vraiment convertible, il est seulement lisible une fois.
yaml.load exécute votre configuration
Le YAML complet gère des étiquettes propres au langage qui construisent des objets arbitraires. En Python, cela signifie qu’un document contenant !!python/object/apply:os.system peut exécuter une commande pendant l’analyse. yaml.safe_load est la version qui ne construit que des types standard, et c’est celle que vous voulez pour tout ce que vous n’avez pas écrit vous-même. PyYAML a fini par rendre l’erreur difficile en exigeant un argument de loader explicite, mais beaucoup de code est antérieur, et bien d’autres langages ont encore un défaut non sûr à un appel de distance.
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f) # pas yaml.load
Le détail du sur-ensemble
YAML 1.2 a été conçu comme un sur-ensemble de JSON, et la spécification affirme que tout document JSON valide est aussi un document YAML 1.2 valide : un analyseur 1.2 lit donc votre JSON. YAML 1.1 pas tout à fait : il veut une espace après le deux-points, donc un {"a":1} compact y est une erreur d’analyse, et les règles de résolution 1.1 transformeront encore certaines de vos chaînes en booléens. Si vous comptez sur « il suffit de donner le JSON à l’analyseur YAML », vérifiez d’abord quelle version votre bibliothèque implémente. Dans tous les cas, vous pouvez faire le trajet inverse proprement avec le convertisseur YAML vers JSON.
TOML : sans ambiguïté, jusqu’à ce que ça s’imbrique
TOML existe parce que les fichiers INI étaient agréables et imprécis. Il corrige l’imprécision : entiers et flottants sont des types distincts, les booléens ne sont que true et false, et il existe quatre vrais types de date et d’heure (date-heure avec décalage, date-heure locale, date locale, heure locale) intégrés à la grammaire plutôt que passés en fraude par des chaînes. Les commentaires sont de première classe. Définir deux fois la même clé est une erreur franche au lieu d’un comportement indéfini, petite chose qui attrape une vraie catégorie d’erreurs de fusion.
Pour une configuration plate ou peu profonde, c’est le meilleur des trois. Cargo.toml et pyproject.toml sont les cas évidents : quelques sections, des valeurs chaînes et listes, une imbrication occasionnelle d’un niveau. Rien n’est ambigu et rien n’a besoin de guillemets par sécurité.
Cela devient vite laid quand les données forment un arbre. Une imbrication profonde impose soit de longs en-têtes pointés, soit de longues clés pointées :
[servers.production.database.replica]
host = "10.0.0.4"
port = 5432
Et un tableau d’objets demande la forme tableau de tables à double crochet, répétée par élément :
[[targets]]
name = "web"
port = 8080
[[targets]]
name = "worker"
port = 8081
Cela se lit bien à deux entrées. À trente entrées de trois champs chacune, avec des tables en ligne qui doivent tenir sur une ligne, vous vous battez contre le format. Si votre configuration est réellement hiérarchique, TOML est la mauvaise forme et vous le sentirez à chaque niveau ajouté.
Ce qu’aucun des trois ne vous donne
Un type décimal. Tous trois vous donnent un flottant, c’est-à-dire une approximation binaire. L’argent appartient toujours aux unités mineures sous forme d’entier, ou à une chaîne.
Des données binaires. JSON et TOML n’ont aucune représentation, donc c’est du base64 dans une chaîne. YAML a une étiquette !!binary, qui fonctionne et ne survit à la conversion vers aucun des deux autres.
Un schéma livré avec le format. JSON Schema est l’option mature, et comme YAML 1.2 se projette sur le même modèle de données, vous pouvez aussi valider du YAML avec. C’est ainsi que fonctionne en pratique la plupart de la validation YAML. TOML n’a pas d’équivalent d’adoption comparable.
Des commentaires qui survivent à une conversion. C’est la porte à sens unique. Les commentaires vivent dans la syntaxe, pas dans le modèle de données : un fichier YAML ou TOML converti en JSON perd tous ses commentaires définitivement, et aucun outillage astucieux ne les récupère. Si les commentaires d’un fichier portent du sens, la source de vérité est ce fichier, et le JSON n’est qu’un artefact que vous générez. L’absence de commentaires en JSON est délibérée, et c’est la raison de cette asymétrie.
Choisir, sous forme de questions
Le seul lecteur est-il une machine ? JSON. Ne faites pas parler YAML à une API.
Un humain l’éditera-t-il chaque semaine, et est-ce hiérarchique ? YAML, avec une discipline de guillemets et safe_load.
Un humain l’éditera-t-il, et est-ce surtout des sections plates de scalaires ? TOML. Vous ne perdez rien et vous gagnez des types sans ambiguïté.
Les commentaires doivent-ils survivre ? Quel que soit votre choix, ce fichier est la source de vérité. Générez vers l’aval, n’éditez jamais la copie générée.
Les valeurs sont-elles produites par des non-développeurs ou par une interface ? JSON, généré par un programme, validé contre un schéma. Chaque piège YAML ci-dessus est déclenché par une chaîne que quelqu’un a tapée.
Êtes-vous en train de convertir entre eux ? Faites-le dans le convertisseur JSON vers YAML et lisez la sortie au lieu de lui faire confiance, en particulier les booléens, puis passez le résultat dans le validateur avant qu’il n’atteigne quoi que ce soit qui déploie.