JSON Toolkit

Soalan Lazim Sintaks dan Format: Komen, Koma Mengekor dan Ketepatan

Inti pati: Sama ada komen dibenarkan, mengapa koma mengekor menimbulkan ralat, adakah kunci perlu petikan, mengapa 9007199254740993 berubah — dua belas kesimpulan, setiap satu disemak mengikut RFC 8259.

Pemula~12 min bacaanDiterbitkan 2026-10-30Dikemas kini 2026-10-30
Kulit untuk soalan lazim sintaks dan format

Pokoknya: tulis JSON RFC 8259 biasa — petikan berganda, tiada komen, tiada koma mengekor, UTF-8 tanpa BOM — untuk apa-apa yang merentasi sempadan proses; gunakan JSONC atau JSON5 untuk konfigurasi yang memerlukan komen dan tukar semula pada masa jalan; dan sirikan integer melebihi 2^53-1 sebagai rentetan di pelayan.

Apa yang sama antara soalan-soalan ini: ia kelihatan seperti "sepatutnya berfungsi", tetapi spesifikasi sebenarnya sangat ketat.

1. Apa yang dikira sebagai JSON sah

Apakah perbezaan antara JSON, JSON5 dan JSONC?

JSON standard hanya mengenali rentetan petikan berganda dan tidak membenarkan komen mahupun koma mengekor. JSON5 dan JSONC kedua-duanya menambah ciri di atasnya:

Ciri JSON (RFC 8259) JSONC JSON5
Rentetan petikan berganda Disokong Disokong Disokong
Rentetan petikan tunggal Tidak disokong Tidak disokong Disokong
Komen Tidak disokong Disokong Disokong
Koma mengekor Tidak disokong Tidak disokong Disokong
Kunci tanpa petikan Tidak disokong Tidak disokong Disokong
Nombor heksadesimal Tidak disokong Tidak disokong Disokong
NaN / Infinity Tidak disokong Tidak disokong Disokong
Sambungan fail .json .jsonc .json5

Prinsip pemilihan mudah: sentiasa gunakan JSON standard merentasi sempadan proses, manakala fail konfigurasi boleh menggunakan JSONC atau JSON5 bergantung pada apa yang disokong oleh editor anda. tsconfig.json dan .vscode/settings.json boleh mengandungi komen kerana editor menghuraikannya sebagai JSONC; memberikannya kepada JSON.parse yang ketat masih akan menimbulkan ralat.

Bolehkah JSON mengandungi komen?

Tidak. Tatabahasa dalam RFC 8259 tidak mempunyai pengeluaran komen, jadi mana-mana // atau /* */ menyebabkan penghuraian gagal.

Jika anda ingin menyimpan penjelasan dalam konfigurasi, terdapat tiga alternatif:

{
  "_comment": "以下超时单位是毫秒",
  "timeout": 3000,
  "retry": {
    "$doc": "重试次数,0 表示不重试",
    "count": 3
  }
}
  • Gunakan medan konvensional seperti _comment yang pembaca hanya abaikan;
  • Gunakan medan berawalan $ untuk mengurangkan peluang bertembung dengan medan perniagaan;
  • Beralih sahaja kepada JSONC atau YAML untuk konfigurasi dan tukar kepada JSON standard pada masa jalan.

Mengapa koma mengekor menyebabkan ralat?

Kerana dalam tatabahasa kedua-dua tatasusunan dan objek ialah struktur "elemen + pemisah", dan tiada koma dibenarkan selepas elemen terakhir.

{
  "a": 1,
  "b": 2,
}

Coretan di atas mempunyai koma tambahan sebelum }, dan ralat biasanya dilaporkan pada koma atau pada pendakap tutup bergantung pada pelaksanaan penghurai. Ini juga perangkap paling mudah apabila berhijrah daripada JavaScript — literal objek JS membenarkan koma mengekor, JSON tidak.

Adakah petikan tunggal dibenarkan?

Tidak. Rentetan mesti menggunakan petikan berganda; 'text' ialah ralat sintaks.

Jika data disalin daripada repr() Python, skrip shell, atau konfigurasi tulisan tangan, tukarkannya dengan alat dahulu dan bukannya berharap penghurai bersikap longgar.

Bolehkah kunci ditulis tanpa petikan?

Tidak. Nama ahli objek mesti rentetan yang dibalut dengan petikan berganda.

{ name: "json" }

Itu ialah literal objek JavaScript, bukan JSON. Menambah petikan menjadikannya sah:

{ "name": "json" }

Bolehkah tahap atas menjadi tatasusunan atau rentetan?

Ya. RFC 8259 membenarkan mana-mana nilai JSON pada tahap atas: objek, tatasusunan, rentetan, nombor, true, false, dan null semuanya sah.

Walau bagaimanapun, dalam amalan kejuruteraan, adalah disyorkan untuk menggunakan hanya objek atau tatasusunan pada tahap atas, atas dua sebab:

  1. Keserasian: penghurai lebih lama (dari era RFC 4627) memerlukan tahap atas menjadi objek atau tatasusunan;
  2. Keselamatan: tatasusunan tahap atas secara sejarah membawa risiko penculikan JSON. Pelayar moden telah membetulkannya, tetapi sistem lama masih menganggap konvensyen itu.

Jadi "hello" ialah JSON sah dan ["a", "b"] juga, tetapi API yang mengembalikan {"data": [...]} lebih selamat.

2. Pengekodan dan rentetan

Adakah BOM di permulaan fail merosakkan penghuraian?

Ya. RFC 8259 secara eksplisit memerlukan teks JSON dihantar sebagai UTF-8 dan tidak boleh menambah tanda urutan bait (BOM).

BOM ialah tiga bait EF BB BF di permulaan fail. Ia benar-benar tidak kelihatan, tetapi ia menduduki kedudukan pertama rentetan, jadi penghurai menyimpulkan bahawa "aksara pertama bukan {" dan menimbulkan ralat — dan baris serta lajur yang dilaporkan biasanya baris 1, lajur 1, yang kelihatan membingungkan.

"Simpan sebagai UTF-8" Windows Notepad menyertakan BOM secara lalai. Untuk menyemak dan membuangnya:

# Semak: output bermula dengan efbbbf bermakna BOM hadir
head -c 3 data.json | xxd

# Buang: langkau 3 bait pertama dengan tail
tail -c +4 data.json > clean.json

JSON.parse JavaScript moden mentoleransi BOM (V8 melangkaunya secara dalaman), tetapi pelaksanaan ketat dalam Java, Go dan Python tidak, yang merupakan sebab biasa "fail yang sama memberikan hasil berbeza dalam bahasa berbeza".

Adakah aksara Cina perlu ditukar kepada \uXXXX?

Tidak. RFC 8259 memerlukan teks JSON menjadi UTF-8, jadi Cina boleh muncul secara langsung; {"name": "张三"} sah sepenuhnya.

\uXXXX hanyalah ejaan alternatif yang setara, digunakan untuk saluran yang tidak dapat mewakili aksara secara langsung. Peraturan am:

  • Badan permintaan HTTP, fail, pangkalan data: tulis Cina secara langsung, menjimatkan 3 bait setiap aksara;
  • Pengepala HTTP, parameter URL, membenamkan di dalam rentetan lain: gunakan escape untuk mengelakkan masalah pengekodan.

Satu-satunya aksara yang mesti di-escape ialah aksara kawalan (U+0000 hingga U+001F). Contohnya, baris baharu mesti ditulis sebagai \n; anda tidak boleh meletakkan baris baharu literal di dalam rentetan.

Bolehkah emoji masuk terus ke dalam rentetan?

Ya, tetapi ingat ia mengambil dua unit kod UTF-16.

Emoji dan banyak aksara jarang (seperti 𠮷) berada di luar BMP, jadi dalam JavaScript "😀".length === 2. Mengesahkan mengikut kiraan aksara oleh itu mudah salah:

const text = "😀";
console.log(text.length);          // 2, bilangan unit kod UTF-16
console.log([...text].length);     // 1, bilangan titik kod
console.log(Buffer.byteLength(text)); // 4, bilangan bait UTF-8

Jika had panjang nama panggilan dikira dengan .length, satu emoji mengambil dua slot; jika backend mengira bait, ia adalah empat. Apabila menyelaraskan definisi, nyatakan dengan jelas sama ada anda bermaksud titik kod, unit kod, atau bait.

3. Nombor dan nilai khas

Mengapa integer besar 9007199254740993 menjadi 9007199254740992?

Kerana JSON.parse JavaScript menukar nombor kepada apungan ketepatan berganda IEEE 754, dan integer selamat terbesar ialah 2^53-1.

Number.MAX_SAFE_INTEGER === 9007199254740991  // true
9007199254740993                              // sebenarnya menghasilkan 9007199254740992

Mana-mana nombor melebihi 2^53-1 mungkin dibundarkan pada digit akhirnya. Mangsa biasa ialah ID pengguna 19 digit yang dijana oleh algoritma snowflake, nombor pesanan, dan cap masa nanosaat.

Tiga pembetulan, disusun mengikut keutamaan:

  1. Sirikan ID sebagai rentetan di pelayan: "id": "9007199254740993", menghapuskan masalah ketepatan pada akarnya;
  2. Gunakan pustaka penghuraian tanpa kehilangan: pengganti JSON.parse boleh mengekalkan integer besar secara verbatim, contohnya json-bigint;
  3. Ekstrak dengan ungkapan nalar untuk paparan sahaja: jangan huraikan keseluruhan dokumen, padankan medan sasaran sebagai teks sahaja.

Perhatikan bahawa JSON.stringify mempunyai masalah simetri: nombor di luar julat integer selamat mungkin tidak lagi mengekalkan nilai asalnya selepas pensirian.

Bolehkah NaN dan Infinity masuk ke dalam JSON?

Tidak. Pengeluaran nombor JSON hanya membenarkan tatatanda perpuluhan, dan NaN, Infinity, dan -Infinity bukan nilai JSON yang sah.

Pendekatan JavaScript ialah "degradasi senyap":

JSON.stringify({ value: NaN });        // '{"value":null}'
JSON.stringify({ value: Infinity });   // '{"value":null}'

Tiada ralat, NaN hanya menjadi null. Ini adalah senario klasik di mana "frontend menghantar NaN dan backend menerima null". Apabila anda perlu menyampaikan semantik sedemikian, nyatakannya secara eksplisit dengan rentetan atau nilai sentinel yang dipersetujui:

{ "ratio": null, "ratioState": "not-a-number" }

Bolehkah nombor ditulis sebagai 1e3 atau 0.5?

Ya. Nombor JSON menyokong perpuluhan dan eksponen, jadi 1e3, 1E3, 0.5, dan -0.5 semuanya sah.

Tetapi terdapat sekatan:

  • Ia tidak boleh bermula atau berakhir dengan .: .5 dan 5. kedua-duanya tidak sah, jadi tulis 0.5 dan 5;
  • Tiada sifar pendahulu: 01 tidak sah;
  • Tiada heksadesimal: 0x1F tidak sah, itu sambungan JSON5;
  • -0 sah, tetapi JSON.stringify(-0) mengeluarkan 0, kehilangan tanda.

4. Semantik objek

Apa yang berlaku dengan kunci pendua?

Tingkah laku ditentukan oleh pelaksanaan; RFC 8259 hanya mengatakan nama kunci "sepatutnya" unik dan tidak menentukan apa yang berlaku pada pendua.

Dalam amalan, sebahagian besar pelaksanaan mengambil nilai terakhir:

JSON.parse('{"a":1,"a":2}')  // { a: 2 }

Tetapi anda tidak boleh bergantung padanya. Sesetengah pustaka Java, encoding/json Go, dan json Python berbeza merentasi versi: sesetengah mengambil nilai pertama, sesetengah menimbulkan ralat, dan sesetengah mengambil yang terakhir. Menganggap kunci pendua sebagai ralat ialah pendekatan paling selamat merentasi bahasa.

Anda tidak boleh menemui kunci pendua dengan mata — dalam JSON yang dikecilkan kunci yang sama mungkin beribu-ribu aksara terpisah. Gunakan skrip:

# Ekstrak kunci yang muncul lebih daripada sekali pada tahap yang sama, sebagai semakan kasar
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d

Apa yang berlaku kepada undefined?

Ia bergantung, dan ini adalah tempat paling mudah menimbulkan bug:

JSON.stringify({ a: undefined });              // '{}'  kunci dibuang sepenuhnya
JSON.stringify({ a: undefined, b: 1 });        // '{"b":1}'
JSON.stringify([undefined]);                   // '[null]'  menjadi null dalam tatasusunan
JSON.stringify({ a: function () {} });         // '{}'  fungsi juga dibuang

undefined yang sama bermaksud "medan hilang" di dalam objek dan "menjadi null" di dalam tatasusunan. Jika backend mengambil cabang berbeza untuk "medan hilang" dan "medan null", anda mendapat ketidakkonsistenan di sini.

Apabila anda perlu menyatakan "tiada nilai" secara eksplisit, gunakan null dan bukannya undefined.

Apa yang berlaku selepas menghuraikan {"__proto__": {...}}?

JSON.parse sendiri selamat: __proto__ diperlakukan sebagai sifat milik biasa dan tidak mengubah prototaip.

Yang berbahaya ialah operasi gabungan selepas penghuraian:

const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch);       // sesetengah pelaksanaan mencemarkan prototaip

Pendekatan yang betul ialah mengulangi sifat milik dan melangkau __proto__, constructor, dan prototype. Butiran ada dalam Soalan Lazim Prestasi dan Keselamatan.

5. Rujukan pantas

Sintaks Sah Nota
{"a": 1,} Tidak Koma mengekor
{'a': 1} Tidak Petikan tunggal
{a: 1} Tidak Kunci tanpa petikan
{"a": 1} // comment Tidak Komen
"hello" Ya Rentetan tahap atas sah, tetapi jarang digunakan dalam API
{"a": 1e3} Ya Eksponen disokong
{"a": 01} Tidak Sifar pendahulu
{"a": NaN} Tidak Literal tidak sah; stringify menukarnya kepada null
\uFEFF{"a":1} Tidak BOM bukan sebahagian daripada teks JSON
{"a":1,"a":2} Dipertikaikan Pelaksanaan mengambil nilai terakhir, tetapi jangan bergantung padanya