JSON, YAML o TOML: quale prendere
Questi tre formati differiscono meno per ciò che sanno esprimere che per il modo in cui falliscono, e sui fallimenti passerai il tuo tempo.
Ogni affermazione di questa pagina è misurata oppure ha una fonte. Quando non è né l’una né l’altra, la pagina lo dice.
Una pipeline di deploy legge un elenco di paesi da un file YAML. Qualcuno aggiunge la Norvegia con il suo codice ISO, NO, e la pipeline comincia a saltare quel mercato senza un errore da nessuna parte. Il valore arrivato all’applicazione era il booleano false.
È questo il genere di cosa che dovrebbe decidere quale formato scegli, non una tabella di «supporta i commenti: sì/no». Tutti e tre i formati sanno contenere una mappa da stringhe a valori. A separarli è ciò che ti fanno quando nessuno guarda.
JSON: noioso, ed è tutto il punto
JSON è un formato di trasporto. Ha sei tipi, quattro caratteri di spaziatura ammessi (spazio, tabulazione, ritorno a capo, avanzamento di riga, secondo la RFC 8259), niente commenti, niente virgole finali, nessun tipo data, ed esattamente un tipo numerico che ogni lettore è libero di interpretare come float64. È sottospecificato in un paio di punti che contano, in particolare le chiavi duplicate, dove la RFC dice che le chiavi DOVREBBERO essere uniche e poi lascia il comportamento indefinito. JavaScript e Python tengono entrambi l’ultima.
Le sue virtù sono del tutto non tecniche. Ogni linguaggio spedisce un parser nella libreria standard. Ogni client HTTP sa cosa farci. Non c’è praticamente disallineamento di versioni: un documento JSON scritto nel 2008 si analizza oggi, in modo identico, ovunque. Quando serializzi per un salto di rete, una riga di log, una coda di messaggi o una cache, nessuna delle funzionalità rivolte agli umani degli altri due formati ti compra qualcosa, e l’universalità ti compra molto.
I modi di fallimento sono ben battuti e riguardano soprattutto i numeri. Number.MAX_SAFE_INTEGER vale 9007199254740991, e gli ID sopra quella soglia vengono riscritti in silenzio, il che merita un articolo a sé. Le date sono stringhe per convenzione e nulla impone la convenzione, il che merita anch’esso un articolo a sé. Nessuno dei due è un motivo per scegliere un altro formato per il trasporto. Sono motivi per stare attenti.
YAML: ergonomia vera, conto vero
Non si sceglie YAML perché è elegante. Lo si sceglie perché un manifesto Kubernetes o una pipeline di CI è una cosa che una persona modifica a mano tutti i giorni, e JSON è davvero sgradevole da modificare a mano: niente commenti, virgolette obbligatorie e una virgola mancante quattrocento righe più su. YAML ti dà commenti, stringhe multiriga leggibili e nessun rumore di punteggiatura. Vale qualcosa.
Ecco cosa stai pagando.
Il problema norvegese
YAML 1.1 risolve no, yes, on, off, y e n senza virgolette come booleani. Lo schema core di YAML 1.2 non lo fa e li lascia stringhe. Stesso documento, stessa chiave, due risposte:
a: no
Con la risoluzione core di YAML 1.2 quel valore è la stringa "no". Con le regole 1.1 è il booleano false. Quale ti tocchi dipende dalla tua libreria, non dal tuo file: PyYAML e il Psych di Ruby risolvono con le regole 1.1, mentre js-yaml segue la 1.2. Il yaml.v3 di Go sta nel mezzo: risolve no come stringa a meno che il campo di destinazione non sia un bool tipizzato, nel qual caso accetta ancora la grafia 1.1. Un servizio Python e uno Node che leggono lo stesso file di configurazione sono in disaccordo sul valore, e nessuno dei due registra nulla.
Il rimedio è virgolettare ogni stringa che possa essere scambiata per altro. Codici paese, numeri di versione (1.10 è un float, "1.10" no), qualsiasi cosa che inizi con uno zero, e qualsiasi valore fornito da un utente. Se generi YAML per via programmatica, fai virgolettare l’emettitore in modo difensivo anziché fidarti della tua revisione.
Gli spazi sono sintassi e le tabulazioni sono vietate
L’indentazione porta la struttura, quindi una riga disallineata è un documento diverso anziché un errore. Peggio, la specifica YAML vieta senz’altro i caratteri di tabulazione per l’indentazione. Un editor configurato per inserire una tabulazione produce un file che fallisce l’analisi con un messaggio su un carattere invisibile nel tuo terminale. Configura l’editor per tipo di file e smetti di pensarci.
Le ancore si espandono in uscita
Ancore e alias ti permettono di definire un blocco una volta e riusarlo:
defaults: &defaults
timeout: 30
retries: 3
staging:
<<: *defaults
host: stage.internal
È la funzionalità che vende YAML a chi mantiene quaranta definizioni di servizio quasi identiche. È anche una funzionalità che il modello dei dati non possiede. Converti quel file in JSON e la chiave di merge viene risolta, l’alias viene espanso e defaults compare per intero dentro staging. Torna indietro a YAML e ottieni due copie letterali. Nulla è sbagliato, propriamente parlando, ma ciò che stavi mantenendo è sparito. Un file YAML che si appoggia sulle ancore non è davvero convertibile, è solo leggibile una volta.
yaml.load esegue la tua configurazione
Lo YAML completo supporta tag specifici del linguaggio che costruiscono oggetti arbitrari. In Python significa che un documento contenente !!python/object/apply:os.system può eseguire un comando durante l’analisi. yaml.safe_load è la versione che costruisce solo tipi standard, ed è quella che vuoi per tutto ciò che non hai scritto tu. PyYAML alla fine ha reso difficile sbagliare richiedendo un argomento loader esplicito, ma parecchio codice è precedente, e parecchi altri linguaggi hanno ancora un valore predefinito non sicuro a una chiamata di distanza.
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f) # non yaml.load
Il dettaglio del sovrainsieme
YAML 1.2 è stato progettato come sovrainsieme di JSON, e la specifica afferma che ogni documento JSON valido è anche un documento YAML 1.2 valido, quindi un parser 1.2 legge il tuo JSON. YAML 1.1 non del tutto: vuole uno spazio dopo i due punti, quindi un {"a":1} compatto lì è un errore di analisi, e le regole di risoluzione 1.1 trasformeranno comunque alcune delle tue stringhe in booleani. Se conti sul «basta dare il JSON al parser YAML», controlla prima quale versione implementa la tua libreria. In ogni caso puoi fare il percorso inverso in modo pulito con il convertitore da YAML a JSON.
TOML: senza ambiguità, finché non si annida
TOML esiste perché i file INI erano piacevoli e imprecisi. Corregge l’imprecisione: interi e float sono tipi distinti, i booleani sono solo true e false, e ci sono quattro veri tipi di data e ora (data-ora con scostamento, data-ora locale, data locale, ora locale) incorporati nella grammatica anziché contrabbandati tramite stringhe. I commenti sono di prima classe. Definire due volte la stessa chiave è un errore netto anziché comportamento indefinito, una piccola cosa che intercetta una classe reale di errori di merge.
Per una configurazione piatta o poco profonda è il migliore dei tre. Cargo.toml e pyproject.toml sono i casi ovvi: poche sezioni, valori stringa e lista, qualche annidamento occasionale di un livello. Nulla è ambiguo e nulla ha bisogno di virgolette per sicurezza.
Diventa brutto in fretta quando i dati sono un albero. L’annidamento profondo impone o intestazioni lunghe puntate o chiavi lunghe puntate:
[servers.production.database.replica]
host = "10.0.0.4"
port = 5432
E un array di oggetti richiede la forma array di tabelle a doppia parentesi, ripetuta per elemento:
[[targets]]
name = "web"
port = 8080
[[targets]]
name = "worker"
port = 8081
Con due voci si legge bene. Con trenta voci da tre campi ciascuna, e tabelle inline che devono stare su una riga, stai combattendo contro il formato. Se la tua configurazione è davvero gerarchica, TOML è la forma sbagliata e lo sentirai a ogni livello che aggiungi.
Ciò che nessuno dei tre ti dà
Un tipo decimale. Tutti e tre ti danno un float, cioè un’approssimazione binaria. Il denaro appartiene ancora alle unità minori come intero, oppure a una stringa.
Dati binari. JSON e TOML non hanno alcuna rappresentazione, quindi è base64 dentro una stringa. YAML ha un tag !!binary, che funziona e non sopravvive alla conversione verso nessuno degli altri due.
Uno schema che arriva col formato. JSON Schema è l’opzione matura, e poiché YAML 1.2 si proietta sullo stesso modello dei dati puoi validare con esso anche lo YAML. È così che funziona in pratica quasi tutta la validazione YAML. TOML non ha un equivalente con adozione paragonabile.
Commenti che attraversino una conversione. Questa è la porta a senso unico. I commenti vivono nella sintassi, non nel modello dei dati, quindi un file YAML o TOML convertito in JSON perde ogni commento in modo permanente, e non esiste strumento ingegnoso che li recuperi. Se i commenti di un file portano peso, la fonte di verità è quel file e il JSON è solo un artefatto che generi. L’assenza di commenti in JSON è deliberata, ed è la ragione per cui questa asimmetria esiste.
Scegliere, sotto forma di domande
L’unico lettore è una macchina? JSON. Non far parlare YAML a un’API.
Lo modificherà una persona ogni settimana, ed è gerarchico? YAML, con disciplina nelle virgolette e safe_load.
Lo modificherà una persona, ed è fatto soprattutto di sezioni piatte di scalari? TOML. Non perdi nulla e guadagni tipi non ambigui.
Ti serve che i commenti sopravvivano? Qualunque cosa tu scelga, quel file è la fonte di verità. Genera verso il basso, non modificare mai la copia generata.
I valori li producono non sviluppatori o un’interfaccia? JSON, generato da un programma, validato contro uno schema. Ogni trappola YAML qui sopra è innescata da una stringa che qualcuno ha digitato.
Stai convertendo tra loro proprio ora? Fallo nel convertitore da JSON a YAML e leggi l’output invece di fidartene, soprattutto i booleani, e passa il risultato dal validatore prima che raggiunga qualcosa che fa deploy.