Sei tipi, precisione numerica e trappole della codifica
In sintesi: JSON ha solo sei tipi, ma ciascuno può rimodellare i tuoi dati senza che tu te ne accorga. Questo articolo copre in un colpo solo le trappole di numeri, stringhe e null.

In sintesi: verifica innanzitutto se i campi ID o gli importi superano 2^53 - 1 o richiedono molte cifre decimali, poi se i limiti delle stringhe contano i code point o le unità UTF-16; trasporta come stringa qualsiasi valore oltre l'intervallo degli interi sicuri invece di fidarti del parser per la precisione.
JSON ha meno tipi di quanto si pensi, e più trappole: i numeri si spostano, le stringhe si rompono e null è una cosa diversa da una chiave mancante.
1. Il panorama dei tipi
| Tipo | Esempio di letterale | Vincolo in JSON | Mapping comune |
|---|---|---|---|
| Oggetto | {"a": 1} |
Le chiavi devono essere stringhe, i valori possono essere qualsiasi cosa | Object / Map / struct |
| Array | [1, "a", null] |
Gli elementi possono essere eterogenei | Array / List / slice |
| Stringa | "text" |
UTF-8, virgolette doppie | String |
| Numero | 1, -1.5, 1e9 |
Niente zeri iniziali, niente +, niente NaN |
double / Decimal |
| Booleano | true / false |
Minuscolo | bool |
| null | null |
L'unico valore null | null / Option::None |
Nota le due cose che non esistono: non c'è un tipo data e non c'è un tipo intero. Tutte le date sono stringhe, e tutti gli interi e i valori in virgola mobile condividono l'unico tipo "number".
2. Numeri: la doppia precisione è la risposta predefinita
2.1 Il confine degli interi sicuri
Il più grande intero rappresentabile esattamente in doppia precisione IEEE 754 è 2^53 - 1 = 9007199254740991. Oltre, compaiono lacune tra interi adiacenti:
JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992
JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'
Non è un problema di JSON; è un problema del passaggio "testo → doppia precisione". Qualsiasi pipeline con JavaScript in mezzo (gateway, BFF, frontend, servizio Node) lo incontrerà.
Rimedi (in ordine di quanto li consiglio):
- Usa stringhe per gli ID lungo tutta la pipeline:
{"id": "9007199254740993"}, il male minore; - Fai il parsing con tipi ad alta precisione sul server:
BigInteger/BigDecimaldi Java,json.Numberdi Go,decimal.Decimaldi Python; - Sul frontend, usa il reviver di
JSON.parseo un parser che supporta i numeri grandi (comejson-bigint) per trasformare il campo inBigInt, ma nota cheBigIntnon può essere passato direttamente aJSON.stringify.
2.2 I decimali non sono valori esatti
0.1 + 0.2 // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"
Rappresenta sempre gli importi monetari come interi nell'unità minima della valuta (centesimi) o come stringhe; non accumularli mai con la virgola mobile.
2.3 Trappole dei numeri durante la serializzazione
JSON.stringify({ a: 1e21 }) // {"a":1e+21}
JSON.stringify({ a: -0 }) // {"a":0}
JSON.stringify({ a: Infinity }) // {"a":null}
JSON.stringify({ a: NaN }) // {"a":null}
JSON.stringify({ a: 10n }) // TypeError: Do not know how to serialize a BigInt
Infinity e NaN vengono trasformati silenziosamente in null, che è la classe più difficile da rintracciare di "i dati sono semplicemente spariti".
2.4 Comportamento predefinito per linguaggio
| Linguaggio | Tipo numerico predefinito | Casi che richiedono gestione esplicita |
|---|---|---|
| JavaScript | double | Interi grandi, serializzazione di BigInt |
| Go | float64 (encoding/json) |
Usa json.Number o Decoder.UseNumber() per preservare la precisione |
| Java (Jackson) | int/long/double/BigDecimal dedotti dal letterale |
Forza DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS |
| Python | int (precisione arbitraria) / float |
Usa parse_float=Decimal per preservare la precisione decimale |
| Rust (serde_json) | i64/u64/f64 |
La precisione arbitraria richiede l'attivazione di arbitrary_precision |
3. Stringhe: un flusso di byte UTF-8 visto attraverso UTF-16
3.1 La lunghezza non è il numero di caratteri
In JavaScript, "🎉".length vale 2, perché gli emoji vivono nei piani supplementari e sono rappresentati da una coppia surrogata.
Se il server limita la lunghezza per "numero di caratteri" mentre il frontend valida con length, le due parti non potranno che essere in disaccordo.
'🎉'.length // 2
Array.from('🎉').length // 1
[...'👨👩👧'].length // 5 (including zero-width joiners)
Quando serve un conteggio rigoroso, usa Intl.Segmenter (per cluster di grafemi) o conta i code point Unicode sul backend, e dichiara chiaramente la definizione nella documentazione dell'API.
3.2 Surrogati isolati
Un surrogato alto isolato come "\ud800" è grammaticalmente JSON valido (\uXXXX accetta qualsiasi quattro cifre esadecimali), ma non è testo Unicode valido.
Da ES2019, JSON.stringify sottopone a escape i surrogati isolati come \ud800, garantendo che l'output sia "ben formato".
Quando si trasferiscono valori simili tra linguaggi diversi, verranno rifiutati se l'altra parte valida Unicode in modo rigoroso.
3.3 \u2028 e \u2029
I separatori di riga e di paragrafo sono validi in JSON, ma incorporarli direttamente nel codice sorgente JavaScript o in un tag <script> causa un errore di sintassi.
Quando si inlinia JSON nell'HTML, sottoponili sempre a escape o passa al parsing da stringa con JSON.parse.
3.4 Tre sorgenti di testo cinese illeggibile
- Il file stesso è in GBK e viene letto come UTF-8;
- L'header della risposta dichiara
charset=gbk, ma il contenuto è in realtà UTF-8; - Il testo attraversa un middleware che "decodifica in stringa e poi ricodifica", quindi il testo cinese viene transcodificato una seconda volta.
Ordine di indagine: guarda prima i byte (xxd), poi l'header della risposta, infine il middleware.
4. null rispetto a "questa chiave non esiste"
In JSON questi due casi sono diversi:
{"nickname": null}
{}
- Il primo significa "il campo esiste, con un valore vuoto";
- Il secondo significa "il campo non esiste".
In TypeScript questo diventa la differenza tra string | null e nickname?: string; in Java diventa la differenza tra "null esplicito" e "valore predefinito".
Senza una convenzione concordata compaiono queste divergenze:
// Frontend A's approach
if (data.nickname === undefined) { /* treat it as having no nickname */ }
// Frontend B's approach
if (!data.nickname) { /* handle null and the empty string together */ }
Raccomandazione: la documentazione dell'API dovrebbe dichiarare "usa null in modo uniforme quando un valore manca, non omettere mai la chiave", oppure l'opposto — ma scegli una sola opzione;
quando aggiorni un'API, usa la semantica PATCH per evitare l'ambiguità "non è stato inviato o è stato inviato come null".
5. Array e oggetti
5.1 Gli array non sono "omogenei"
[1, "two", {"three": 3}, null]
Grammaticalmente valido, ma la definizione del tipo diventa molto difficile da scrivere. Nelle API, cerca di mantenere omogenei gli elementi degli array; per i casi eterogenei, passa a un array di oggetti con un campo discriminante type.
5.2 Le chiavi degli oggetti non sono ordinate, ma molti dipendono dall'ordine
La specifica afferma che un oggetto è "una collezione non ordinata di coppie nome/valore". In pratica V8 preserva l'ordine di inserimento (tranne le chiavi intere, che vengono ordinate). Costruire logica sull'ordine delle chiavi (prendendo "la prima chiave", per esempio) significa dipendere da un dettaglio implementativo.
5.3 Array sparsi e buchi non esistono
[1, , 3] in JavaScript si serializza come [1,null,3] — il buco diventa null. JSON non ha il concetto di "slot".
6. Una checklist su cui puoi agire
- [ ] Tutti gli ID, gli importi monetari e gli interi sovradimensionati sono esplicitamente stringhe nel contratto dell'API;
- [ ] La documentazione dell'API dichiara la semantica di
nullrispetto a un campo omesso; - [ ] I limiti di lunghezza delle stringhe indicano se contano code point o unità UTF-16;
- [ ] Prima della serializzazione vengono filtrati
Infinity/NaN/undefined, oppure vengono convertiti esplicitamente innull; - [ ] Il JSON inlinato nell'HTML è sottoposto a escape;
- [ ] Frontend e backend hanno ciascuno un caso di test contrattuale che copre "intero grande + emoji + null".
FAQ
Come devo gestire gli interi superiori a 2^53 - 1?
L'approccio meno problematico è inviare campi come gli ID come stringhe lungo tutta la pipeline, ad esempio {"id": "9007199254740993"}. Sul server puoi preservare la precisione con BigInteger / BigDecimal di Java, json.Number di Go o decimal.Decimal di Python. Sul frontend, un parser come json-bigint può trasformare il campo in BigInt, ma ricorda che BigInt non può essere passato direttamente a JSON.stringify.
Perché Infinity e NaN diventano null dopo la serializzazione?
Il tipo number di JSON non accetta letterali non standard come NaN e Infinity, quindi JSON.stringify li converte silenziosamente in null senza alcun errore. È la classe più difficile da rintracciare di "i dati sono semplicemente spariti". Filtra questi valori esplicitamente prima della serializzazione, oppure convertili in null secondo la tua convenzione di business e registralo nei log.
Devo inviare null o omettere del tutto il campo?
Entrambe le opzioni funzionano, ma la documentazione dell'API deve sceglierne una e dichiararla chiaramente, altrimenti ogni frontend presumerà qualcosa di diverso. Negli esempi, {"nickname": null} e {} corrispondono a string | null e nickname?: string in TypeScript. Usa la semantica PATCH quando aggiorni un'API per evitare l'ambiguità "non è stato inviato o è stato inviato come null".