JSON Toolkit

Enam jenis, ketepatan nombor, dan perangkap pengekodan

Inti pati: JSON hanya mempunyai enam jenis, tetapi setiap satunya boleh mengubah bentuk data anda secara senyap. Artikel ini merangkumi perangkap nombor, rentetan, dan null dalam satu tinjauan.

Pemula~12 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk Enam jenis, ketepatan nombor, dan perangkap pengekodan

Kesimpulan utama: semak dahulu sama ada medan ID atau wang melebihi 2^53 - 1 atau memerlukan banyak tempat perpuluhan ketepatan, kemudian semak sama ada had rentetan mengira titik kod atau unit UTF-16; bawa mana-mana nilai di luar julat integer selamat sebagai rentetan dan jangan percayakan penghurai untuk mengekalkan ketepatan.

JSON mempunyai lebih sedikit jenis daripada yang difikirkan kebanyakan orang, dan lebih banyak perangkap: nombor beralih, rentetan rosak, dan null ialah perkara yang berbeza daripada kunci yang tiada.

1. Landskap jenis

Jenis Contoh literal Kekangan dalam JSON Pemetaan lazim
Objek {"a": 1} Kunci mesti rentetan, nilai boleh apa sahaja Object / Map / struct
Tatasusunan [1, "a", null] Elemen boleh heterogen Array / List / slice
Rentetan "text" UTF-8, tanda petikan berganda String
Nombor 1, -1.5, 1e9 Tiada sifar di hadapan, tiada +, tiada NaN double / Decimal
Boolean true / false Huruf kecil bool
null null Satu-satunya nilai null null / Option::None

Perhatikan dua perkara yang tidak wujud: tiada jenis tarikh, dan tiada jenis integer. Semua tarikh ialah rentetan, dan semua integer serta nilai titik terapung berkongsi satu jenis "nombor" yang sama.

2. Nombor: ketepatan berganda ialah jawapan lalai

2.1 Sempadan integer selamat

Integer terbesar yang boleh diwakili dengan tepat oleh titik terapung ketepatan berganda IEEE 754 ialah 2^53 - 1 = 9007199254740991. Melebihi itu, jurang muncul antara integer bersebelahan:

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

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

Ini bukan masalah JSON; ini masalah dengan langkah "teks → ketepatan berganda". Mana-mana saluran yang mempunyai JavaScript di tengahnya (gateway, BFF, frontend, perkhidmatan Node) akan menemuinya.

Penyelesaian (disusun mengikut betapa saya mengesyorkannya):

  1. Gunakan rentetan untuk ID sepanjang saluran: {"id": "9007199254740993"}, paling sedikit masalah antara semua;
  2. Hurai dengan jenis berketepatan tinggi di pelayan: BigInteger / BigDecimal Java, json.Number Go, decimal.Decimal Python;
  3. Di frontend, gunakan reviver JSON.parse atau penghurai yang menyokong nombor besar (seperti json-bigint) untuk menukar medan itu menjadi BigInt, tetapi ambil perhatian bahawa BigInt tidak boleh dihantar terus ke JSON.stringify.

2.2 Perpuluhan bukan nilai tepat

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

Sentiasa wakilkan jumlah wang sebagai integer dalam unit mata wang terkecil (sen) atau sebagai rentetan; jangan sekali-kali mengumpulnya dengan titik terapung.

2.3 Perangkap nombor semasa penyerialan

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 dan NaN ditukar menjadi null secara senyap, dan ini adalah kelas "data itu terus hilang" yang paling sukar dikesan.

2.4 Tingkah laku lalai mengikut bahasa

Bahasa Jenis nombor lalai Kes yang memerlukan pengendalian eksplisit
JavaScript double Integer besar, penyerialan BigInt
Go float64 (encoding/json) Gunakan json.Number atau Decoder.UseNumber() untuk mengekalkan ketepatan
Java (Jackson) int/long/double/BigDecimal yang disimpulkan daripada literal Paksa DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
Python int (ketepatan sewenang-wenangnya) / float Gunakan parse_float=Decimal untuk mengekalkan ketepatan perpuluhan
Rust (serde_json) i64/u64/f64 Ketepatan sewenang-wenangnya memerlukan pengaktifan arbitrary_precision

3. Rentetan: aliran bait UTF-8 dilihat melalui UTF-16

3.1 Panjang tidak sama dengan bilangan aksara

Dalam JavaScript, "🎉".length ialah 2, kerana emoji berada dalam satah tambahan dan diwakili oleh pasangan surrogate. Jika pelayan mengehadkan panjang mengikut "bilangan aksara" manakala frontend mengesahkan dengan length, kedua-dua pihak dijamin tidak sependapat.

'🎉'.length                 // 2
Array.from('🎉').length     // 1
[...'👨‍👩‍👧'].length           // 5 (including zero-width joiners)

Apabila anda memerlukan pengiraan yang ketat, gunakan Intl.Segmenter (mengikut gugusan grapheme) atau kira titik kod Unicode di bahagian belakang, dan nyatakan definisi itu dengan jelas dalam dokumentasi API.

3.2 Surrogate tunggal

Surrogate tinggi tunggal seperti "\ud800" adalah JSON yang sah dari segi tatabahasa (\uXXXX menerima mana-mana empat digit hex), tetapi ia bukan teks Unicode yang sah. Sejak ES2019, JSON.stringify meng-escape surrogate tunggal sebagai \ud800, menjamin bahawa output itu "terbentuk dengan baik". Apabila dipindahkan merentas bahasa, nilai seperti ini akan ditolak jika pihak lain mengesahkan Unicode dengan ketat.

3.3 \u2028 dan \u2029

Pemisah baris dan pemisah perenggan adalah sah dalam JSON, tetapi membenamkannya terus ke dalam kod sumber JavaScript atau teg <script> menyebabkan kesilapan sintaks. Apabila menyisipkan JSON ke dalam HTML, sentiasa escape-kannya atau beralih kepada penghuraian daripada rentetan dengan JSON.parse.

3.4 Tiga punca teks Cina yang rosak

  1. Fail itu sendiri ialah GBK dan dibaca sebagai UTF-8;
  2. Pengepala respons menyatakan charset=gbk, tetapi kandungannya sebenarnya UTF-8;
  3. Ia melalui middleware yang "menyahkod kepada rentetan dan kemudian mengekod semula", jadi teks Cina ditranskodkan kali kedua.

Urutan siasatan: lihat bait dahulu (xxd), kemudian pengepala respons, dan akhirnya middleware.

4. null berbanding "kunci ini tidak wujud"

Dalam JSON kedua-duanya berbeza:

{"nickname": null}
{}
  • Yang pertama bermaksud "medan itu wujud, dengan nilai kosong";
  • Yang kedua bermaksud "medan itu tidak wujud".

Dalam TypeScript ini menjadi perbezaan antara string | null dan nickname?: string; dalam Java ia menjadi perbezaan antara "null eksplisit" dan "nilai lalai". Tanpa konvensyen yang dipersetujui, perpecahan ini muncul:

// 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 */ }

Syor: dokumentasi API harus menyatakan "gunakan null secara seragam apabila nilai tiada, jangan sekali-kali tinggalkan kunci", atau sebaliknya — tetapi pilih satu sahaja; apabila mengemas kini API, gunakan semantik PATCH untuk mengelakkan kekaburan "adakah ia tidak dihantar, atau adakah ia dihantar sebagai null".

5. Tatasusunan dan objek

5.1 Tatasusunan bukan "homogen"

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

Sah dari segi tatabahasa, tetapi definisi jenis menjadi sangat sukar untuk ditulis. Dalam API, cuba kekalkan elemen tatasusunan homogen; bagi kes heterogen, beralih kepada tatasusunan objek dengan medan pembezalayan type.

5.2 Kunci objek tidak tersusun, namun ramai orang bergantung pada susunan

Spesifikasi menyatakan objek ialah "koleksi pasangan nama/nilai yang tidak tersusun". Dalam amalan V8 mengekalkan susunan sisipan (kecuali kunci integer, yang diisih). Membina logik berdasarkan susunan kunci (contohnya mengambil "kunci pertama") bermaksud bergantung pada perincian pelaksanaan.

5.3 Tatasusunan jarang dan lubang tidak wujud

[1, , 3] dalam JavaScript diserialkan menjadi [1,null,3] — lubang itu menjadi null. JSON tidak mempunyai konsep "slot".

6. Senarai semak yang boleh dilaksanakan

  • [ ] Semua ID, jumlah wang, dan integer bersaiz berlebihan dinyatakan secara eksplisit sebagai rentetan dalam kontrak API;
  • [ ] Dokumentasi API menyatakan semantik null berbanding medan yang ditinggalkan;
  • [ ] Had panjang rentetan menyatakan sama ada ia mengira titik kod atau unit UTF-16;
  • [ ] Tapis Infinity / NaN / undefined sebelum penyerialan, atau tukarkannya kepada null secara eksplisit;
  • [ ] JSON yang disisipkan ke dalam HTML di-escape;
  • [ ] Frontend dan backend masing-masing mempunyai satu kes ujian kontrak yang merangkumi "integer besar + emoji + null".

Soalan Lazim

Bagaimana saya harus mengendalikan integer melebihi 2^53 - 1?

Pendekatan yang paling sedikit masalah ialah menghantar medan seperti ID sebagai rentetan sepanjang saluran, contohnya {"id": "9007199254740993"}. Di pelayan anda boleh mengekalkan ketepatan dengan BigInteger / BigDecimal Java, json.Number Go, atau decimal.Decimal Python. Di frontend, penghurai seperti json-bigint boleh menukar medan itu menjadi BigInt, tetapi ingat bahawa BigInt tidak boleh dihantar terus ke JSON.stringify.

Mengapa Infinity dan NaN menjadi null selepas penyerialan?

Jenis nombor JSON tidak menerima literal bukan standard seperti NaN dan Infinity, jadi JSON.stringify menukarnya menjadi null secara senyap tanpa sebarang kesilapan. Ini ialah kelas "data itu terus hilang" yang paling sukar dikesan. Tapis nilai ini secara eksplisit sebelum penyerialan, atau tukarkannya kepada null mengikut konvensyen perniagaan anda dan logkannya.

Patutkah saya menghantar null atau meninggalkan medan itu sepenuhnya?

Mana-mana pun boleh, tetapi dokumentasi API mesti memilih satu dan menyatakannya dengan jelas, jika tidak frontend akan masing-masing menganggap sesuatu yang berbeza. Dalam contoh, {"nickname": null} dan {} dipetakan kepada string | null dan nickname?: string dalam TypeScript. Gunakan semantik PATCH apabila mengemas kini API untuk mengelakkan kekaburan "adakah ia tidak dihantar, atau adakah ia dihantar sebagai null".