Leggere i confini della grammatica JSON da RFC 8259
In sintesi: Tradurre le produzioni grammaticali di RFC 8259 in parole semplici: quali caratteri possono comparire direttamente, quali devono essere sottoposti a escape e dove la specifica lascia deliberatamente margine.

In sintesi: quando compare un errore di sintassi JSON, individua il primo carattere illegale e classificalo come apice singolo, virgola finale, commento o carattere di controllo invece di rileggere tutto il documento; sul fronte della codifica, verifica che sia UTF-8 senza BOM e trasporta come stringhe gli interi oltre 2^53 - 1.
Il corpo della specifica occupa solo una dozzina di pagine, ma dietro ogni "MUST", "SHOULD" e "MAY" si nasconde un incidente che ha tenuto un team sveglio tutta la notte.
1. Come si rapportano i tre testi
Imparare JSON crea spesso confusione per via di tre numeri: RFC 4627, RFC 7159, RFC 8259, più ECMA-404. Il rapporto è in realtà abbastanza chiaro:
| Testo | Data | Stato | Differenza principale |
|---|---|---|---|
| RFC 4627 | 2006 | Superato | Il livello superiore deve essere un oggetto o un array |
| RFC 7159 | 2014 | Superato | Livello superiore allentato a qualsiasi valore JSON |
| RFC 8259 | 2017.12 | Standard attuale | UTF-8 esplicito, aggiunte indicazioni di interoperabilità |
| ECMA-404 | 2013, seconda edizione 2017.12 | Specifica grammaticale | Definisce solo la grammatica, non copre codifica né trasporto |
Quale leggere? Basta RFC 8259. ECMA-404 è equivalente nella parte grammaticale; la differenza è che RFC 8259 specifica anche "trasporto come UTF-8" e "i problemi di interoperabilità a cui gli implementatori devono fare attenzione".
2. Sei tipi di valore
Le produzioni grammaticali della specifica si possono condensare in un unico schema:
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
Ci sono solo sei punti chiave:
- Gli oggetti usano le graffe, i membri sono separati da virgole e una virgola dopo l'ultimo membro non è ammessa;
- Gli array usano le parentesi quadre, gli elementi sono separati da virgole e anche le virgole finali non sono ammesse;
- I numeri non ammettono
+iniziale, né zeri iniziali (01non è valido), né un punto decimale isolato (1.non è valido), néNaNoInfinity; - Le stringhe devono usare le virgolette doppie; gli apici singoli non sono validi;
- Booleani e null sono letterali minuscoli;
TrueeNULLsono entrambi non validi; - Tra gli elementi strutturali possono comparire solo i caratteri di spaziatura
0x20(spazio),0x09(tab),0x0A(line feed) e0x0D(ritorno carrello).
Il frammento seguente è JSON valido, anche se molti lo considerano sbagliato la prima volta che lo vedono — perché è una stringa nuda di livello superiore:
"just a string"
Dopo RFC 7159 il livello superiore può davvero essere qualsiasi valore. Nelle API, però, si consiglia comunque di usare solo oggetti o array; il motivo è nella sezione 6.
3. Cosa può comparire davvero direttamente dentro una stringa
Questa è la parte in cui ci si inganna più facilmente con "sulla mia macchina funziona". Le regole della specifica per le stringhe sono:
- Caratteri ammessi direttamente:
%x20-21,%x23-5B,%x5D-10FFFF; - In altre parole, le virgolette doppie
"(0x22) e la barra rovesciata\(0x5C) devono essere sottoposte a escape; - Tutti i caratteri di controllo sotto 0x20 devono essere sottoposti a escape, inclusi line feed, tab e ritorno carrello;
- Le uniche sequenze di escape disponibili sono
\",\\,\/,\b,\f,\n,\r,\te\uXXXX.
{
"ok": "Chinese text, emoji 🎉 and full-width symbols can be written directly",
"escaped": "A line break goes here\nand a tab\tfinishes it",
"control": "\u0007 is the bell character and must be escaped",
"quote": "He said: \"don't use single quotes\""
}
\/ è un escape legacy (impedisce che </script> chiuda prematuramente il tag). Nelle applicazioni moderne serve raramente, ma i parser devono supportarlo.
4. Codifica: UTF-8, senza BOM
La sezione 8.1 di RFC 8259 afferma tre cose:
- Il testo JSON usato per lo scambio deve essere codificato in UTF-8;
- Le implementazioni non devono aggiungere un BOM (
EF BB BF) all'inizio del testo; - I parser possono ignorare il BOM per essere più robusti.
Due errori tipici nel mondo reale:
- Il Blocco note di Windows salva come "UTF-8 con BOM" e il file viene rifiutato da
JSON.parseconUnexpected token; - Un file JSON cinese salvato in GBK diventa testo illeggibile su una pipeline che decodifica solo come UTF-8, ma la sintassi riesce comunque a passare, quindi l'errore viene rinviato al livello applicativo.
La soluzione è semplice: rimuovere il BOM in fase di lettura del file e poi decodificare esplicitamente come UTF-8; in scrittura, assicurarsi che non venga aggiunto alcun BOM.
/**
* Removes the UTF-8 BOM at the start of a string.
* @param text The original text
* @returns The text without a BOM
*/
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
Una nota sui media type, già che ci siamo: application/json non accetta più il parametro charset; scriverlo non genera un errore, ma è una pratica obsoleta.
5. Tre punti in cui la specifica lascia deliberatamente margine
I punti in cui la specifica rifiuta di fissare le cose sono esattamente l'origine delle differenze tra implementazioni.
1. Intervallo e precisione dei numeri
RFC 8259 dice solo che "questa specifica non limita l'intervallo o la precisione dei numeri", osservando che la maggior parte delle implementazioni usa la doppia precisione IEEE 754.
Ciò significa che lo stesso 9223372036854775807 diventa 9223372036854776000 in JavaScript, mentre resta esatto nel long di Java.
Conclusione: gli interi oltre 2^53 - 1 e gli importi monetari che richiedono molte cifre decimali dovrebbero sempre essere trasportati come stringhe, con il pattern scritto esplicitamente nello schema.
2. Chiavi duplicate
La formulazione della specifica è "i nomi all'interno di un oggetto dovrebbero (SHOULD) essere unici"; non dice cosa fare quando non lo sono.
JSON.parseconserva l'ultimo;- Alcuni parser conservano il primo;
- Alcune librerie sollevano semplicemente un errore.
Conclusione: non scrivere chiavi duplicate e non affidarti a un comportamento particolare nei tuoi validator. Aggiungere in fase di build un passaggio di "rilevamento chiavi duplicate" costa molto meno che indagare dopo.
3. Limiti di profondità e lunghezza
La specifica non fissa alcun limite alla profondità di annidamento, ma ogni implementazione ne ha uno. V8 genera RangeError: Maximum call stack size exceeded a decine di migliaia di livelli di annidamento.
I server devono imporre i propri limiti; è un requisito di sicurezza, non della specifica.
6. Verificare la comprensione con controesempi
Ogni riga seguente è JSON non valido. Passale a un parser e osserva se i messaggi di errore sono utili:
{ 'name': 'json' } // single quotes
{ name: "json" } // key name not quoted
{"a": 1,} // trailing comma
[1, 2, 3,] // trailing comma in an array
{"a": 01} // leading zero
{"a": +1} // leading plus sign
{"a": .5} // missing integer part
{"a": NaN} // non-standard literal
{"a": undefined} // not a JSON value
{"a": 1} // comment // comment
Un caso limite interessante: {"a": 1} seguito da un commento è valido in JSON5, valido in JSONC e non valido nel JSON standard.
Quindi "il file di configurazione funziona ma l'API lo rifiuta" è quasi sempre un problema di dialetto.
7. Checklist per la produzione
- [ ] Tutto il JSON trasmesso esternamente usa
application/jsonsenzacharset; - [ ] Le pipeline di lettura/scrittura dei file rimuovono il BOM e usano UTF-8 esplicito;
- [ ] Gli interi oltre
2^53sono trasportati come stringhe e annotati nello schema; - [ ] Le chiavi duplicate vengono rilevate in fase di build;
- [ ] Il server limita la dimensione del corpo della richiesta e la profondità di annidamento;
- [ ] I valori di livello superiore delle API pubbliche sono oggetti o array, mai scalari nudi.
FAQ
Come faccio a capire rapidamente se un testo è JSON valido?
Consegna il testo a un parser o a uno strumento di parsing online, leggi la posizione del primo carattere illegale e confrontala con i sei tipi di valore e le regole di spaziatura di RFC 8259. Il livello superiore può essere qualsiasi valore JSON, ma tra gli elementi strutturali sono ammessi solo spazio, tab, line feed e ritorno carrello. Un apice singolo, una chiave senza virgolette, una virgola finale o un commento significano che non è JSON standard.
Perché JSON deve usare UTF-8 e non deve avere un BOM?
La sezione 8.1 di RFC 8259 richiede che il testo JSON usato per lo scambio sia codificato in UTF-8, afferma che le implementazioni non devono aggiungere un BOM all'inizio e consente ai parser di ignorarlo per robustezza. Un file con BOM viene rifiutato da JSON.parse con Unexpected token, anche se a occhio non sembra esserci nulla di strano. Quindi rimuovi il BOM in lettura e decodifica esplicitamente come UTF-8.
Conviene usare JSON5 o JSONC per aggirare questi limiti grammaticali?
Sì, ma solo per la lettura locale, ad esempio i file di configurazione; i dati scambiati tra sistemi dovrebbero comunque essere JSON standard. I controesempi mostrano che {"a": 1} seguito da un commento è valido in JSON5 e JSONC ma non nel JSON standard, motivo per cui "il file di configurazione funziona ma l'API lo rifiuta" è quasi sempre un problema di dialetto. In fase di conversione, rimuovi prima commenti, apici singoli e virgole finali e passa il risultato a un parser standard.