JSON Toolkit

Šest typů, přesnost čísel a úskalí kódování

Shrnutí: JSON má jen šest typů, ale každý z nich může vaše data tiše přetvořit. Tento článek v jednom průchodu pokrývá úskalí čísel, řetězců a null.

Začátečník~12 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku Šest typů, přesnost čísel a úskalí kódování

Stručně: nejprve zkontrolujte, zda pole s ID nebo peněžní částkou přesahují 2^53 - 1 nebo vyžadují mnoho desetinných míst, poté zkontrolujte, zda limity délky řetězců počítají kódové body, nebo jednotky UTF-16; každou hodnotu mimo rozsah bezpečných celých čísel přenášejte jako řetězec, místo abyste se spoléhali na to, že parser přesnost zachová.

JSON má méně typů, než si většina lidí myslí, a více úskalí: čísla se posunou, řetězce se rozbijí a null je něco jiného než chybějící klíč.

1. Přehled typů

Typ Příklad literálu Omezení v JSON Běžné mapování
Objekt {"a": 1} Klíče musí být řetězce, hodnoty mohou být cokoli Object / Map / struct
Pole [1, "a", null] Prvky mohou být heterogenní Array / List / slice
Řetězec "text" UTF-8, dvojité uvozovky String
Číslo 1, -1.5, 1e9 Žádné úvodní nuly, žádné +, žádné NaN double / Decimal
Boolean true / false Malá písmena bool
null null Jediná hodnota null null / Option::None

Všimněte si dvou věcí, které neexistují: neexistuje typ datum a neexistuje typ celé číslo. Všechna data jsou řetězce a všechna celá a desetinná čísla sdílejí jediný typ „number“.

2. Čísla: dvojitá přesnost je výchozí odpověď

2.1 Hranice bezpečných celých čísel

Největší celé číslo, které IEEE 754 s dvojitou přesností dokáže přesně reprezentovat, je 2^53 - 1 = 9007199254740991. Za touto hranicí se mezi sousedními celými čísly objevují mezery:

JSON.parse('{"id": 9007199254740993}').id
// 9007199254740992

JSON.stringify({ id: 9007199254740993 })
// '{"id":9007199254740992}'

Není to problém JSON, je to problém kroku „text → dvojitá přesnost“. Narazí na něj každá pipeline, v níž je uprostřed JavaScript (brána, BFF, frontend, služba Node).

Řešení (seřazená podle toho, jak je doporučuji):

  1. Používejte řetězce pro ID v celé pipeline: {"id": "9007199254740993"}, nejméně problémů ze všech;
  2. Na serveru parsujte typy s vysokou přesností: BigInteger / BigDecimal v Javě, json.Number v Go, decimal.Decimal v Pythonu;
  3. Na frontendu použijte reviver JSON.parse nebo parser podporující velká čísla (například json-bigint) a převeďte pole na BigInt, pamatujte však, že BigInt nelze předat přímo do JSON.stringify.

2.2 Desetinná čísla nejsou přesné hodnoty

0.1 + 0.2            // 0.30000000000000004
JSON.stringify(0.1 + 0.2)
// "0.30000000000000004"

Peněžní částky vždy reprezentujte jako celá čísla v nejmenší měnové jednotce (centech) nebo jako řetězce; nikdy je nesčítejte v plovoucí desetinné čárce.

2.3 Úskalí čísel při serializaci

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 a NaN se tiše změní na null, což je nejhůře dohledatelná třída „data prostě zmizela“.

2.4 Výchozí chování podle jazyka

Jazyk Výchozí číselný typ Případy vyžadující explicitní ošetření
JavaScript double Velká celá čísla, serializace BigInt
Go float64 (encoding/json) Použijte json.Number nebo Decoder.UseNumber() pro zachování přesnosti
Java (Jackson) int/long/double/BigDecimal odvozené z literálu Vynuťte DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
Python int (libovolná přesnost) / float Použijte parse_float=Decimal pro zachování přesnosti desetinných čísel
Rust (serde_json) i64/u64/f64 Libovolná přesnost vyžaduje zapnutí arbitrary_precision

3. Řetězce: bajtový proud UTF-8 nazíraný přes UTF-16

3.1 Délka není totéž co počet znaků

V JavaScriptu je "🎉".length rovno 2, protože emoji žijí v doplňkových rovinách a jsou reprezentována surrogate párem. Pokud server omezuje délku podle „počtu znaků“, zatímco frontend validuje pomocí length, obě strany se zaručeně neshodnou.

'🎉'.length                 // 2
Array.from('🎉').length     // 1
[...'👨‍👩‍👧'].length           // 5 (včetně spojek s nulovou šířkou)

Když potřebujete rigorózní počítání, použijte Intl.Segmenter (podle grapheme clusterů) nebo počítejte kódové body Unicode na backendu a definici výslovně uveďte v dokumentaci API.

3.2 Osamocené surrogate

Osamocený vysoký surrogate, například "\ud800", je gramaticky platný JSON (\uXXXX přijímá libovolné čtyři hexadecimální číslice), ale není to platný text Unicode. Od ES2019 JSON.stringify escapuje osamocené surrogate jako \ud800, čímž zaručuje, že výstup je „well-formed“. Při přenosu mezi jazyky budou takové hodnoty odmítnuty, pokud protější strana validuje Unicode přísně.

3.3 \u2028 a \u2029

Oddělovače řádku a odstavce jsou v JSON platné, ale jejich přímé vložení do zdrojového kódu JavaScriptu nebo do značky <script> způsobí syntaktickou chybu. Při inline vkládání JSON do HTML je vždy escapujte, nebo přejděte k parsování ze řetězce pomocí JSON.parse.

3.4 Tři zdroje rozsypaného čínského textu

  1. Soubor je sám v GBK a čte se jako UTF-8;
  2. Hlavička odpovědi deklaruje charset=gbk, ale obsah je ve skutečnosti UTF-8;
  3. Data procházejí middlewarem, který „dekóduje na řetězec a poté znovu kóduje“, takže čínský text se překóduje podruhé.

Pořadí vyšetřování: nejprve se podívejte na bajty (xxd), poté na hlavičku odpovědi a nakonec na middleware.

4. null versus „tento klíč neexistuje“

V JSON jsou tyto dva případy rozdílné:

{"nickname": null}
{}
  • První znamená „pole existuje, s prázdnou hodnotou“;
  • Druhý znamená „pole neexistuje“.

V TypeScriptu se z toho stane rozdíl mezi string | null a nickname?: string; v Javě rozdíl mezi „explicitním null“ a „výchozí hodnotou“. Bez dohodnuté konvence vznikají tato rozbití:

// Přístup frontendu A
if (data.nickname === undefined) { /* považovat za chybějící přezdívku */ }
// Přístup frontendu B
if (!data.nickname) { /* ošetřit null i prázdný řetězec společně */ }

Doporučení: dokumentace API má uvést „když hodnota chybí, používejte jednotně null, klíč nikdy nevynechávejte“, nebo opak – ale vyberte jen jednu variantu; při úpravách API používejte sémantiku PATCH, abyste se vyhnuli dvojznačnosti „neposlali, nebo poslali null“.

5. Pole a objekty

5.1 Pole nejsou „homogenní“

[1, "two", {"three": 3}, null]

Gramaticky platné, ale definici typu je pak velmi obtížné napsat. V API se snažte udržet prvky pole homogenní; pro heterogenní případy přejděte k poli objektů s diskriminačním polem type.

5.2 Klíče objektů nejsou uspořádané, přesto na pořadí mnozí spoléhají

Specifikace říká, že objekt je „neuspořádaná kolekce dvojic název/hodnota“. V praxi V8 zachovává pořadí vkládání (kromě celočíselných klíčů, které se řadí). Stavět logiku na pořadí klíčů (například brát „první klíč“) znamená spoléhat na detail implementace.

5.3 Řídká pole a díry neexistují

JavaScriptové [1, , 3] se serializuje jako [1,null,3] – díra se změní na null. JSON pojem „slot“ nemá.

6. Kontrolní seznam, podle kterého můžete jednat

  • [ ] Všechna ID, peněžní částky a příliš velká celá čísla jsou ve smlouvě API výslovně řetězci;
  • [ ] Dokumentace API uvádí sémantiku null oproti vynechanému poli;
  • [ ] Limity délky řetězců uvádějí, zda počítají kódové body, nebo jednotky UTF-16;
  • [ ] Před serializací odfiltrujte Infinity / NaN / undefined, nebo je explicitně převeďte na null;
  • [ ] JSON vložený do HTML je escapovaný;
  • [ ] Frontend i backend mají shodný testovací případ pokrývající „velké celé číslo + emoji + null“.

Časté dotazy

Jak zacházet s celými čísly nad 2^53 - 1?

Nejmenší potíže způsobí posílat pole, jako jsou ID, jako řetězce v celé pipeline, například {"id": "9007199254740993"}. Na serveru můžete přesnost zachovat pomocí BigInteger / BigDecimal v Javě, json.Number v Go nebo decimal.Decimal v Pythonu. Na frontendu může parser jako json-bigint převést pole na BigInt, pamatujte však, že BigInt nelze předat přímo do JSON.stringify.

Proč se Infinity a NaN po serializaci změní na null?

Číselný typ JSON nepřijímá ne‑standardní literály jako NaN a Infinity, proto je JSON.stringify tiše převede na null bez jakékoli chyby. Je to nejhůře dohledatelná třída „data prostě zmizela“. Tyto hodnoty před serializací explicitně odfiltrujte, nebo je podle obchodní konvence převeďte na null a zaznamenejte to.

Má se posílat null, nebo pole úplně vynechat?

Funguje obojí, ale dokumentace API musí jednu variantu zvolit a jasně ji uvést, jinak každý frontend předpokládá něco jiného. V příkladech se {"nickname": null} a {} mapují na string | null a nickname?: string v TypeScriptu. Při úpravách API používejte sémantiku PATCH, abyste se vyhnuli dvojznačnosti „neposlali, nebo poslali null“.