Vai al contenuto
OneKitly

YAML sembra amichevole e morde

Pubblicato il 12/08/2025 · 18 min di lettura · Strumenti per sviluppatori

Daniel Okonkwo

Daniel OkonkwoSviluppatore front-end e redattore Tech presso OneKitly

Performance web · Formati di file

Verificato su 6 fonti

Vedi il profilo
In breve

YAML 1.2 dichiara JSON un sottoinsieme, quindi ogni documento JSON è YAML valido. Ciò che YAML aggiunge sopra è un passo di risoluzione che indovina un tipo per ogni scalare non quotato, e quell'indovinello è cambiato fra le versioni della specifica. Sotto YAML 1.1 i token y, yes, no, on e off si risolvono in booleani: è il famoso problema norvegese, in cui il codice paese NO diventa false. YAML 1.2 li ha tolti dallo schema di base, quindi un analizzatore 1.2 li lascia come stringhe. Quale comportamento ottieni dipende interamente dal tuo analizzatore, non dal tuo file. Misurato su uno stesso documento: js-yaml 4.3.0, che si descrive come analizzatore YAML 1.2, restituisce la stringa "no"; PyYAML 6.0.3, un analizzatore YAML 1.1, restituisce False. La stessa frattura colpisce 01234, che vale 1234 in 1.2 e l'ottale 668 in 1.1, e 12:30:00, stringa in 1.2 e intero 45000 in 1.1. Alcune insidie hanno superato intatte il cambio di versione: 1.10 è il decimale 1,1 in entrambi, quindi un numero di versione perde una cifra in silenzio. Aggiungi l'indentazione significativa in cui le tabulazioni sono del tutto vietate, due stili di scalare a blocco con tre modi di troncatura, e àncore che portano 403 byte a 39 MB: la regola pratica si scrive da sola — quota ogni stringa che potrebbe essere letta come altro.

YAML è JSON più uno strato di inferenza dei tipi, ed è l'inferenza la parte pericolosa. Lo stesso file passato in un analizzatore YAML 1.2 e in uno 1.1: no è una stringa nell'uno e false nell'altro, 01234 è 1234 nell'uno e 668 nell'altro, e 12:30:00 è un numero in uno dei due.

Un soprainsieme di JSON, più un'idea pericolosa

YAML 1.2 enuncia la relazione esplicitamente: JSON è un sottoinsieme di YAML, e un processore YAML conforme accetta qualsiasi documento JSON. Eseguilo e l'affermazione regge — dare {"a": 1, "b": [1,2,3]} a js-yaml restituisce esattamente l'oggetto atteso. Quindi tutto ciò che l'articolo precedente diceva sui tipi mancanti di JSON vale anche qui, immutato. YAML non ti dà un tipo intero distinto da un decimale, né un tipo binario, né uno schema.

Ciò che YAML aggiunge è comodità per gli umani: commenti, niente virgolette sulle chiavi, niente virgolette sulla maggior parte delle stringhe, niente graffe, niente virgole, testo a blocco che conserva le sue interruzioni di riga, e un meccanismo di riferimento per scrivere un valore una volta e riusarlo. Sono miglioramenti reali per un file mantenuto da una persona, ed è per questo che YAML governa la configurazione della maggior parte degli strumenti di deployment in uso oggi.

L'idea pericolosa è proprio quella che rende tutto ciò possibile. Se chiavi e stringhe non hanno bisogno di virgolette, l'analizzatore deve decidere cosa significa un token non quotato, e YAML chiama quel passo risoluzione: uno scalare semplice viene confrontato con un insieme di espressioni regolari e gli viene assegnato un tipo. Da lì viene ogni sorpresa di questo articolo, ed è anche la parte del linguaggio che è cambiata fra YAML 1.1 e YAML 1.2 — perciò lo stesso file può significare due cose diverse a seconda della libreria che lo legge.

Il problema norvegese, e quale versione della specifica hai davvero

YAML 1.1 definiva un tipo booleano dal repertorio generoso: true e false, ma anche yes e no, on e off, e nel repository dei tipi le lettere singole y e n. Un file che elenca codici paese trasforma dunque la Norvegia in un booleano, perché il codice ISO della Norvegia è NO. È tutto il famoso bug, e non è un difetto dell'analizzatore: è la specifica che funziona come è scritta.

YAML 1.2 lo ha risolto restringendo lo schema di base. Il tag booleano ora corrisponde solo a true e false, in poche capitalizzazioni. Tutto il resto resta una stringa. Ma il tuo file non porta una versione, e quasi nessuno scrive la direttiva %YAML che ne dichiarerebbe una, quindi la versione che vale è quella che la tua libreria implementa — ed entrambe sono ancora largamente in produzione oggi.

Verifica, quindi, non dare per scontato. Il percorso YAML di questo sito usa js-yaml 4.3.0, la cui descrizione di pacchetto recita «YAML 1.2 parser and serializer», e passando il documento di prova al suo interno si ottiene la stringa "no" per il caso norvegese — sotto tutti e quattro i suoi schemi, dal failsafe al predefinito. Lo stesso documento attraverso PyYAML 6.0.3, un analizzatore YAML 1.1, restituisce il False di Python. Stessi byte, significati opposti, e nessuna delle due librerie sbaglia. Prenditi tre minuti e passa il tuo file nel tuo analizzatore prima di credere a qualsiasi cosa scritta online su questo, incluso questo.

Numeri che non sono quelli che hai digitato

La più costosa ha superato intatta il cambio di versione. Scrivi version: 1.10 ed entrambi gli analizzatori restituiscono il numero in virgola mobile 1,1 — js-yaml dà 1.1, PyYAML dà 1.1 come float. YAML l'ha risolto come numero, e un numero non ha zero finale: la tua versione 1.10 è ora la versione 1.1 e si ordina prima di 1.2 e 1.9. Quotala e sopravvive: "1.10" torna come stringa 1.10 in entrambi. I numeri di versione, di parte, di modello e tutto ciò che ha una cifra finale significativa vanno quotati, in ogni versione della specifica.

Gli zeri iniziali sono peggio, perché le due versioni non concordano sul come. Scrivi zip: 01234 e js-yaml restituisce il numero 1234 — lo zero iniziale è semplicemente sparito, perché il pattern intero di YAML 1.2 è un decimale ordinario. PyYAML restituisce 668, perché sotto YAML 1.1 uno zero iniziale significa ottale, e 1234 letto in base 8 vale 668. Quindi un codice postale, un codice di filiale o un numero di conto scritto senza virgolette viene corrotto da entrambi gli analizzatori, in due valori sbagliati diversi. Nel verso opposto, la notazione ottale propria di YAML 1.2, 0o17, si risolve in 15 con js-yaml e resta la stringa "0o17" con PyYAML, che non riconosce la forma più recente.

L'ultima trappola numerica è la sessagesimale, ed è morta con YAML 1.1. Quella versione risolveva i gruppi di cifre separati da due punti come interi in base 60: 12:30:00 diventava 45000 e 22:22 diventava 1342. Esegui: PyYAML restituisce esattamente quei due interi, e js-yaml restituisce le stringhe. Un orario alla crontab, una durata, un frammento di indirizzo MAC o un riferimento temporale musicale scritto senza virgolette in un file 1.1 diventa un intero senza rapporto evidente con ciò che hai scritto — 45000 è il numero di secondi in dodici ore e mezza, il che è quantomeno logico, e 1342 è 22 per 60 più 22, che non è ciò che intendeva nessuno.

Spazi, tabulazioni vietate e i due scalari a blocco

L'indentazione è struttura in YAML, il che significa che gli spazi non sono cosmetici e un formattatore non può riflusseggiarli liberamente. La specifica vieta del tutto le tabulazioni nell'indentazione — non sconsiglia, vieta — perché una tabulazione non ha larghezza definita e l'analizzatore non avrebbe modo di sapere quanto in profondità volevi essere. Dai una lista indentata con tabulazioni a js-yaml e si ferma con una lamentela precisa: tab characters must not be used in indentation, riga 2 colonna 1. Quell'errore è il fallimento YAML più comune in un editor che converte premurosamente gli spazi iniziali.

Il testo multiriga usa uno di due stili a blocco, e la differenza è esattamente cosa succede alle tue interruzioni di riga. Lo stile letterale, scritto con una barra verticale, conserva ogni interruzione: un blocco di due righe torna come "line one\nline two\n". Lo stile ripiegato, scritto con un segno di maggiore, unisce le righe consecutive con uno spazio: lo stesso blocco torna come "line one line two\n". Il ripiegato conserva però una riga vuota come interruzione vera, quindi un blocco ripiegato di due paragrafi restituisce "para one line a para one line b\npara two\n" — un paragrafo unito e poi una rottura genuina.

Sopra lo stile c'è un indicatore di troncatura che decide l'interruzione di riga finale, ed è la parte che si dimentica. Il valore predefinito, scritto senza nulla in più, è clip: si conserva esattamente un'interruzione finale. Un segno meno la elimina, quindi lo stesso blocco torna come "line one\nline two" senza alcuna interruzione finale. Un segno più conserva tutte le righe vuote finali: un blocco seguito da una riga vuota restituisce "line one\nline two\n\n". Conta più di quanto sembri: un certificato, una chiave SSH o uno script di shell incorporato in un file di configurazione di solito ha bisogno della sua interruzione finale, mentre un token o una password incorporata di solito non deve averla. Sbagliare produce una discrepanza su un valore che sembra identico in ogni diff.

Àncore e alias: una funzione vera che è anche una bomba

Un'àncora nomina un nodo con una e commerciale, un alias vi rimanda con un asterisco, e la chiave di fusione tira le chiavi di una mappa dentro un'altra. Insieme eliminano la maggiore fonte di deriva nella configurazione: scrivi i tuoi valori predefiniti una volta e poi sovrascrivi i due che cambiano per ambiente. Eseguilo e fa esattamente ciò che vuoi — un blocco base con un timeout di 30 e tre tentativi, fuso in dev con il timeout portato a 5, dà a dev un timeout di 5 e tre tentativi, mentre prod conserva 30 e 3.

C'è una sottigliezza da conoscere prima di farci affidamento: un alias non copia, condivide. Carica un documento in cui due voci di lista fanno alias sulla stessa àncora e le due voci sono lo stesso oggetto — l'uguaglianza stretta fra loro è vera, e anche con l'originale. Muta una dopo il caricamento e le hai mutate tutte. È innocuo per configurazione in sola lettura e una trappola vera in codice che normalizza o corregge l'albero caricato sul posto.

Proprio quella condivisione rende possibile l'attacco per espansione. Concatena àncore in modo che ogni livello sia una lista di nove riferimenti al livello sottostante: la dimensione del documento logico è nove elevato alla profondità mentre il file resta minuscolo. Misurato con js-yaml: quattro livelli sono 241 byte di YAML e 54 127 byte di JSON, un fattore 225; sei livelli sono 349 byte e 4,38 MB, un fattore 12 563; sette livelli sono 403 byte e 39,46 MB, un fattore 97 914. A nove livelli il conteggio dei nodi logici arriva a 387 420 489. Nota dove cade il costo reale: js-yaml ha analizzato tutto questo in meno di due millisecondi, perché gli alias sono riferimenti condivisi e il grafo in memoria resta piccolo. È stato serializzare il risultato a impiegare 363 millisecondi a sette livelli. Quindi la difesa non è solo un limite dell'analizzatore: è rifiutarsi di percorrere, copiare in profondità o serializzare un albero proveniente da YAML non fidato, più un tetto di dimensione sull'ingresso e un limite di espansione degli alias se la tua libreria ne offre uno.

La regola, e cosa un formattatore può e non può fare per te

Quota ogni stringa che potrebbe essere letta come altro. In pratica è una lista corta e memorizzabile: tutto ciò che è o contiene yes, no, on, off, y, n, true o false; ogni codice paese, specialmente NO; ogni valore con zero iniziale; ogni numero di versione o di parte con zero finale dopo una virgola decimale; tutto ciò che contiene due punti, come un'ora o una durata; le parole null e none e la tilde; e tutto ciò che sembra un numero ma è in realtà un identificatore. Gli apici singoli sono la forma più sicura, perché al loro interno nulla è un escape: un percorso Windows o un'espressione regolare passa intatto.

Una cosa vale la pena chiarirla, perché è un equivoco comune. Il formattatore YAML di questo sito non analizza YAML. Normalizza il testo — converte le tabulazioni in due spazi, toglie gli spazi finali, comprime le serie di righe vuote e, per minificare, elimina commenti e righe vuote — e controlla a parte il solo errore duro, una tabulazione nell'indentazione. Non risolve mai uno scalare, quindi non può trasformare il tuo NO in false né il tuo 1.10 in 1,1, e non riformatterà i tuoi scalari a blocco. È deliberato: un formattatore che facesse fare al tuo file un giro attraverso un analizzatore applicherebbe in silenzio la versione delle regole di risoluzione di quell'analizzatore e ti restituirebbe un documento diverso.

Per la stessa ragione, tratta ogni conversione da YAML a JSON come un passo con perdita e ispeziona il risultato. Convertire è esattamente il momento in cui scattano le regole di risoluzione, quindi è anche il modo più economico per scoprire cosa il tuo analizzatore pensa davvero che dica il tuo file — dagli la tua configurazione, leggi il JSON, e ogni errore di quoting di questo articolo diventa visibile in una passata.

Lo stesso documento in due analizzatori: js-yaml 4.3.0 (analizzatore YAML 1.2) e PyYAML 6.0.3 (analizzatore YAML 1.1)
Scritto nel filejs-yaml 4.3.0 (YAML 1.2)PyYAML 6.0.3 (YAML 1.1)Forma sicura
no"no" (stringa)False (booleano)'no'
NO (il codice ISO della Norvegia)"NO" (stringa)False (booleano)'NO'
yes"yes" (stringa)True (booleano)'yes' o true
1.10 (un numero di versione)1,1 (numero — lo zero è sparito)1,1 (float — lo zero è sparito)"1.10"
01234 (un codice postale)1234 (numero — decimale)668 (intero — letto in ottale)"01234"
12:30:00 (un'ora del giorno)"12:30:00" (stringa)45000 (intero — base 60)"12:30:00"
0o17 (notazione ottale di YAML 1.2)15 (numero)"0o17" (stringa — forma ignota alla 1.1)Scrivi invece il valore decimale
Formattatore / validatore YAMLRiordina lo YAML — normalizza l'indentazione, converte le tabulazioni in spazi e segnala gli errori di tabulazione.Prova lo strumento

Domande frequenti

Il problema norvegese è risolto, e come capisco quale versione implementa il mio analizzatore?
È risolto nella specifica e non necessariamente nel tuo programma. YAML 1.2 ha tolto yes, no, on e off dal tag booleano dello schema di base, quindi un analizzatore 1.2 li lascia come stringhe. YAML 1.1 li risolveva tutti, più le lettere singole y e n del suo repository dei tipi, ed è per questo che il codice paese ISO NO diventava false. Il tuo file non dichiara una versione — la direttiva %YAML esiste ma praticamente nessuno la scrive — quindi il comportamento viene interamente dalla libreria. La prova affidabile richiede un minuto: carica un documento di due righe con una chiave il cui valore semplice sia no, e stampa il tipo del risultato. Misurato qui, js-yaml 4.3.0 restituisce la stringa "no", e lo fa sotto tutti e quattro gli schemi che include, dal failsafe al predefinito. PyYAML 6.0.3 restituisce il False di Python. Entrambe sono implementazioni corrette di versioni diverse della specifica. Nota anche che le implementazioni divergono sulle forme a una lettera perfino dentro la 1.1 — PyYAML lascia una y nuda e una n nuda come stringhe — quindi provare batte leggere. E qualunque sia la risposta, quotare il valore è gratis e funziona in ogni versione.
Perché il mio numero di versione 1.10 è diventato 1,1?
Perché ha corrisposto al pattern del decimale, e un decimale non ha memoria degli zeri finali. Entrambi gli analizzatori concordano qui — js-yaml restituisce 1.1 e PyYAML restituisce 1.1 come float — quindi questa non è una questione di versione della specifica e quotare è l'unico rimedio. Il danno va oltre un difetto di visualizzazione. L'ordinamento si rompe, perché come numero 1,1 sta fra 1,09 e 1,2 mentre come stringa di versione 1.10 va dopo 1.9. L'uguaglianza si rompe, perché una ricerca della versione chiamata 1.10 non trova più la chiave. E riserializzare il file riscrive 1.1 su disco: l'errore diventa permanente nel tuo repository e il diff mostra una modifica di un carattere dall'aria plausibile. La stessa trappola cattura ogni identificatore puntato a due componenti: un numero di capitolo, una revisione di firmware, una versione di schema, un codice prodotto decimale. Scrivilo come "1.10" fra virgolette. Se ti serve una vera semantica d'ordine, usa una versione semantica a tre componenti: contiene due punti e quindi non può corrispondere al pattern del decimale — 1.10.0 è una stringa in ogni analizzatore anche senza virgolette, benché quotarla comunque non costi nulla e tolga la necessità di pensarci.
Quando uso la barra verticale e quando il segno di maggiore?
Usa la barra verticale, lo stile letterale, ogni volta che le interruzioni di riga fanno parte del valore: uno script di shell, un certificato, una chiave SSH, un'istruzione SQL, un file di configurazione incorporato, un diagramma ASCII. Misurato, un blocco letterale di due righe restituisce "line one\nline two\n" — ogni interruzione preservata, più una alla fine. Usa il segno di maggiore, lo stile ripiegato, per prosa che vuoi spezzare nel file sorgente ma conservare su una sola riga: una descrizione lunga, un messaggio di aiuto, un modello di commit. Lo stesso blocco ripiegato restituisce "line one line two\n" — l'interruzione interna è diventata uno spazio. Il ripiegato rispetta comunque le righe vuote come separazioni di paragrafo: un blocco ripiegato con una riga vuota in mezzo restituisce "para one line a para one line b\npara two\n". Scegli poi l'indicatore di troncatura deliberatamente. La forma nuda conserva esattamente un'interruzione finale, un segno meno la elimina del tutto, e un segno più le conserva tutte. Un certificato PEM ha bisogno della sua interruzione finale: la forma nuda è quella giusta. Un token o un segreto su una riga non deve averla: usa il meno. È questo il dettaglio che produce la misteriosa discrepanza di firma o l'errore di analisi di openssl su un valore che sembra corretto nel file.
Le àncore e gli alias sono sicuri nella configurazione di produzione?
Nei file che scrivi e revisioni, sì — sono lo strumento giusto per valori predefiniti condivisi, e la chiave di fusione produce esattamente il pattern di sovrascrittura per ambiente di cui la maggior parte dei deployment ha bisogno. Due avvertenze valgono anche lì. Un alias condivide il nodo invece di copiarlo, verificato qui dall'uguaglianza stretta fra due voci con alias, quindi qualsiasi codice che muta l'albero caricato sul posto cambierà tutte le occorrenze in una volta. E la chiave di fusione è una funzione di YAML 1.1 trascinata per convenzione anziché una parte dello schema di base 1.2, quindi il supporto varia per libreria — controlla la tua prima di farci affidamento. Nei file che arrivano da fuori della tua organizzazione, tratta gli alias come un vettore di esaurimento di risorse. Un ventaglio da nove su sette livelli qui è stato 403 byte in ingresso e 39,46 MB in uscita, un fattore 97 914, e nove livelli arriverebbero a 387 420 489 nodi logici. Vale la pena sapere con precisione dove cade il costo: js-yaml ha analizzato ognuno di quei casi in meno di due millisecondi, perché gli alias restano riferimenti condivisi; il conto da 363 millisecondi è arrivato serializzando il risultato. Quindi la difesa è un tetto di dimensione sull'ingresso, un limite di espansione degli alias se la tua libreria ne espone uno, e una regola contro il copiare in profondità o serializzare un albero caricato da YAML non fidato.
Il formattatore YAML di questo sito cambia il significato dei miei valori?
No, perché non li analizza mai. Lavora a livello di testo: sostituisce le tabulazioni con due spazi, toglie gli spazi finali da ogni riga, comprime le serie di tre o più righe vuote a una, e in modalità minify elimina le righe di commento e quelle vuote. Esegue inoltre una convalida, il solo errore duro che la specifica definisce per gli spazi, e riporta il numero di riga di ogni tabulazione trovata nell'indentazione. Poiché nessuno scalare viene mai risolto, un NO nudo resta i due caratteri NO, 1.10 conserva il suo zero finale, e i tuoi scalari a blocco tornano esattamente come li hai scritti. È una scelta di progetto deliberata: un formattatore costruito su un analizzatore farebbe passare il tuo file attraverso le regole di risoluzione di quell'analizzatore e ti restituirebbe un documento con valori diversi, che è precisamente il guasto di cui parla questo articolo. Se vuoi vedere come si risolve il tuo file, convertilo invece in JSON — è quello il passo in cui avviene la risoluzione, e leggere il JSON è l'audit più rapido del tuo quoting.

Articoli che potrebbero interessarti

Tutte le guide
SpiegazioneDa XML a JSON: attributi, ripetizione e la trappola dell'array a un solo elementoDue documenti che differiscono solo per quanti figli esistono producono due forme JSON diverse, e senza uno schema nessun convertitore può distinguerli. Più ciò che questo fa davvero con attributi, contenuto misto e spazi — e l'unica cosa che continua a non poter registrare.ConfrontoJSON vs XML: qual è la differenza?JSON e XML memorizzano entrambi dati strutturati come testo, ma con compromessi diversi. Ecco come appare ciascuno, dove vince ciascuno e come scegliere.SpiegazioneJSON è più semplice di quanto pensi, ed è questo il problemaJSON non ha un tipo intero, né un tipo data, né commenti, né schema. Ognuna di queste assenze produce un bug preciso: un identificatore di 19 cifre torna sbagliato di 21, un timestamp diventa una stringa su cui nessuno si è accordato, NaN non si può scrivere e le chiavi duplicate sono legali. Tutto eseguito, in due linguaggi.GuidaFormattare o minificare: a cosa serve ciascuno e cosa cambia sul pesoQuattro fogli di stile veri passati nel minificatore, misurati grezzi e dopo gzip. Togliere tutti gli spazi ha fatto risparmiare 48, 103, 104 e 147 byte compressi; togliere i commenti 57, 1 358, 2 420 e 1 042. E i cinque input che questo minificatore rompe.TutorialCostruire una tabella Markdown da zero, senza contare i trattini a manoLa cosa più piccola che sia ancora una tabella sono due righe: una riga di intestazione e una riga delimitatrice. Ecco perché la seconda è obbligatoria in GitHub Flavored Markdown, dove le tabelle a barre non esistono affatto, e che cosa fa un generatore che scrivere a mano non può fare.GuidaIncollare una tabella in una pull request: che cosa si rompe, e i due caratteri che la romponoUna tabella Markdown vieta esattamente due caratteri dentro una cella: la barra verticale e l'a capo. Ecco che cosa fa ciascuno, come li tratta un convertitore, perché l'escape va applicato nell'ordine giusto, e perché il riempimento non conta mai.

Strumenti correlati

Fonti

Hai notato un errore in questo articolo?