Six types, précision des nombres et pièges d'encodage
L’essentiel : JSON ne compte que six types, mais chacun peut remodeler vos données sans bruit. Cet article traite d'un seul tenant les pièges des nombres, des chaînes et de null.

L'essentiel : vérifiez d'abord si des champs d'identifiant ou de montant dépassent 2^53 - 1 ou exigent beaucoup de décimales, puis si les limites de chaîne comptent des points de code ou des unités UTF-16 ; transportez toute valeur au-delà de la plage entière sûre sous forme de chaîne au lieu de compter sur le parseur pour préserver la précision.
JSON a moins de types que la plupart des gens ne le pensent, et plus de pièges : les nombres dérivent, les chaînes cassent, et null n'est pas la même chose qu'une clé absente.
1. Le paysage des types
| Type | Exemple littéral | Contrainte en JSON | Mapping courant |
|---|---|---|---|
| Objet | {"a": 1} |
Les clés doivent être des chaînes, les valeurs peuvent être quelconques | Object / Map / struct |
| Tableau | [1, "a", null] |
Les éléments peuvent être hétérogènes | Array / List / slice |
| Chaîne | "text" |
UTF-8, guillemets doubles | String |
| Nombre | 1, -1.5, 1e9 |
Pas de zéros initiaux, pas de +, pas de NaN |
double / Decimal |
| Booléen | true / false |
Minuscules | bool |
| null | null |
La seule valeur nulle | null / Option::None |
Notez les deux choses qui n'existent pas : il n'y a pas de type date, et il n'y a pas de type entier. Toutes les dates sont des chaînes, et tous les entiers et flottants partagent le même type « nombre ».
2. Nombres : la double précision est la réponse par défaut
2.1 La frontière de l'entier sûr
Le plus grand entier que le double précision IEEE 754 peut représenter exactement est 2^53 - 1 = 9007199254740991. Au-delà, des trous apparaissent entre entiers adjacents :
JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992
JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'
Ce n'est pas un problème JSON ; c'est un problème de l'étape « texte → double précision ». Tout pipeline avec du JavaScript au milieu (passerelle, BFF, frontend, service Node) y sera exposé.
Remèdes (classés par ordre de recommandation) :
- Utilisez des chaînes pour les identifiants tout au long du pipeline :
{"id": "9007199254740993"}, le moins de tracas de tous ; - Parsez avec des types haute précision côté serveur :
BigInteger/BigDecimalde Java,json.Numberde Go,decimal.Decimalde Python ; - Côté frontend, utilisez le reviver de
JSON.parseou un parseur qui prend en charge les grands nombres (commejson-bigint) pour transformer le champ enBigInt, mais notez queBigIntne peut pas être passé directement àJSON.stringify.
2.2 Les décimales ne sont pas des valeurs exactes
0.1 + 0.2 // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"
Représentez toujours les montants monétaires en entiers dans la plus petite unité (centimes) ou en chaînes ; ne les accumulez jamais en flottant.
2.3 Pièges des nombres à la sérialisation
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 et NaN sont transformés silencieusement en null, ce qui est la catégorie la plus difficile à traquer de « la donnée a simplement disparu ».
2.4 Comportement par défaut selon les langages
| Langage | Type numérique par défaut | Cas nécessitant un traitement explicite |
|---|---|---|
| JavaScript | double | Grands entiers, sérialisation de BigInt |
| Go | float64 (encoding/json) |
Utilisez json.Number ou Decoder.UseNumber() pour préserver la précision |
| Java (Jackson) | int/long/double/BigDecimal inféré depuis le littéral |
Forcez DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS |
| Python | int (précision arbitraire) / float |
Utilisez parse_float=Decimal pour préserver la précision décimale |
| Rust (serde_json) | i64/u64/f64 |
La précision arbitraire exige d'activer arbitrary_precision |
3. Chaînes : un flux d'octets UTF-8 vu à travers UTF-16
3.1 La longueur n'est pas le nombre de caractères
En JavaScript, "🎉".length vaut 2, parce que les emoji vivent dans les plans supplémentaires et sont représentés par une paire de substituts.
Si le serveur limite la longueur par « nombre de caractères » tandis que le frontend valide par length, les deux côtés seront garantis en désaccord.
'🎉'.length // 2
Array.from('🎉').length // 1
[...'👨👩👧'].length // 5 (including zero-width joiners)
Quand il faut un comptage rigoureux, utilisez Intl.Segmenter (par grappe de graphèmes) ou comptez les points de code Unicode côté backend, et énoncez clairement la définition dans la documentation de l'API.
3.2 Substituts isolés
Un substitut haut isolé comme "\ud800" est grammaticalement du JSON valide (\uXXXX accepte quatre chiffres hexadécimaux quelconques), mais ce n'est pas du texte Unicode valide.
Depuis ES2019, JSON.stringify échappe les substituts isolés en \ud800, garantissant que la sortie est « bien formée ».
Lors de transferts entre langages, ces valeurs seront rejetées si l'autre côté valide Unicode strictement.
3.3 \u2028 et \u2029
Les séparateurs de ligne et de paragraphe sont valides en JSON, mais les intégrer directement dans du code source JavaScript ou une balise <script> provoque une erreur de syntaxe.
Quand vous intégrez du JSON dans du HTML, échappez-les toujours ou passez à un parsing depuis une chaîne avec JSON.parse.
3.4 Trois sources de texte chinois brouillé
- Le fichier lui-même est en GBK et est lu en UTF-8 ;
- L'en-tête de réponse déclare
charset=gbk, mais le contenu est en réalité de l'UTF-8 ; - Il traverse un middleware qui « décode en chaîne puis réencode », donc le texte chinois est transcodé une seconde fois.
Ordre d'investigation : regardez d'abord les octets (xxd), puis l'en-tête de réponse, et enfin le middleware.
4. null contre « cette clé n'existe pas »
En JSON ces deux sont différents :
{"nickname": null}
{}
- Le premier signifie « le champ existe, avec une valeur vide » ;
- Le second signifie « le champ n'existe pas ».
En TypeScript cela devient la différence entre string | null et nickname?: string ; en Java la différence entre « null explicite » et « valeur par défaut ».
Sans convention convenu, ces divergences apparaissent :
// 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 */ }
Recommandation : la documentation d'API devrait énoncer « utilisez null uniformément quand une valeur est absente, n'omettez jamais la clé », ou l'inverse — mais choisissez-en un seul ;
lors d'une mise à jour d'API, utilisez la sémantique PATCH pour éviter l'ambiguïté « n'a pas été envoyé, ou envoyé comme null ».
5. Tableaux et objets
5.1 Les tableaux ne sont pas « homogènes »
[1, "two", {"three": 3}, null]
Grammaticalement valide, mais la définition de type devient très difficile à écrire. Dans les API, essayez de garder les éléments de tableau homogènes ; pour les cas hétérogènes, passez à un tableau d'objets avec un champ discriminant type.
5.2 Les clés d'objet sont non ordonnées, et pourtant beaucoup dépendent de l'ordre
La spécification dit qu'un objet est « une collection non ordonnée de paires nom/valeur ». En pratique V8 préserve l'ordre d'insertion (sauf les clés entières, qui sont triées). Bâtir de la logique sur l'ordre des clés (prendre « la première clé », par exemple) signifie dépendre d'un détail d'implémentation.
5.3 Les tableaux creux et les trous n'existent pas
Le [1, , 3] de JavaScript se sérialise en [1,null,3] — le trou devient null. JSON n'a aucun concept d'« emplacement ».
6. Une liste de vérification actionnable
- [ ] Tous les identifiants, montants et entiers surdimensionnés sont explicitement des chaînes dans le contrat d'API ;
- [ ] La documentation d'API énonce la sémantique de
nullcontre un champ omis ; - [ ] Les limites de longueur de chaîne précisent si elles comptent des points de code ou des unités UTF-16 ;
- [ ] Filtrez
Infinity/NaN/undefinedavant sérialisation, ou convertissez-les explicitement ennull; - [ ] Le JSON intégré dans du HTML est échappé ;
- [ ] Frontend et backend ont chacun un cas de test de contrat couvrant « grand entier + emoji + null ».
FAQ
Comment gérer les entiers au-delà de 2^53 - 1 ?
L'approche la moins pénible est d'envoyer des champs comme les identifiants sous forme de chaînes tout au long du pipeline, par exemple {"id": "9007199254740993"}. Côté serveur, vous pouvez préserver la précision avec BigInteger / BigDecimal de Java, json.Number de Go ou decimal.Decimal de Python. Côté frontend, un parseur comme json-bigint peut transformer le champ en BigInt, mais rappelez-vous que BigInt ne peut pas être passé directement à JSON.stringify.
Pourquoi Infinity et NaN deviennent-ils null après sérialisation ?
Le type nombre JSON n'accepte pas les littéraux non standard comme NaN et Infinity, donc JSON.stringify les convertit silencieusement en null sans aucune erreur. C'est la catégorie la plus difficile à traquer de « la donnée a simplement disparu ». Filtrez explicitement ces valeurs avant sérialisation, ou convertissez-les en null selon votre convention métier et journalisez-le.
Faut-il envoyer null ou omettre entièrement le champ ?
Les deux fonctionnent, mais la documentation d'API doit en choisir un et l'énoncer clairement, sinon chaque frontend supposera quelque chose de différent. Dans les exemples, {"nickname": null} et {} correspondent à string | null et nickname?: string en TypeScript. Utilisez la sémantique PATCH lors d'une mise à jour d'API pour éviter l'ambiguïté « n'a pas été envoyé, ou envoyé comme null ».