Câu hỏi thường gặp về cú pháp và định dạng: chú thích, dấu phẩy cuối và độ chính xác
Kết luận: Chú thích có được phép không, tại sao dấu phẩy cuối lại lỗi, khóa có cần nháy không, tại sao 9007199254740993 biến đổi — mười hai kết luận, mỗi kết luận đối chiếu với RFC 8259.

Tóm lại: hãy viết JSON thuần RFC 8259 — nháy kép, không chú thích, không dấu phẩy cuối, UTF-8 không có BOM — cho mọi thứ đi qua ranh giới tiến trình; dùng JSONC hoặc JSON5 cho cấu hình cần chú thích rồi chuyển lại lúc chạy; và trên máy chủ, hãy tuần tự hóa các số nguyên vượt quá 2^53-1 thành chuỗi.
Điểm chung của những câu hỏi này: chúng trông như "phải hoạt động được", nhưng đặc tả thực ra rất chặt.
1. Những gì được coi là JSON hợp lệ
JSON, JSON5 và JSONC khác nhau thế nào?
JSON chuẩn chỉ công nhận chuỗi trong nháy kép và không cho phép chú thích lẫn dấu phẩy cuối. JSON5 và JSONC đều bổ sung tính năng lên trên nó:
| Tính năng | JSON (RFC 8259) | JSONC | JSON5 |
|---|---|---|---|
| Chuỗi nháy kép | Hỗ trợ | Hỗ trợ | Hỗ trợ |
| Chuỗi nháy đơn | Không hỗ trợ | Không hỗ trợ | Hỗ trợ |
| Chú thích | Không hỗ trợ | Hỗ trợ | Hỗ trợ |
| Dấu phẩy cuối | Không hỗ trợ | Không hỗ trợ | Hỗ trợ |
| Khóa không có nháy | Không hỗ trợ | Không hỗ trợ | Hỗ trợ |
| Số thập lục phân | Không hỗ trợ | Không hỗ trợ | Hỗ trợ |
NaN / Infinity |
Không hỗ trợ | Không hỗ trợ | Hỗ trợ |
| Phần mở rộng tệp | .json |
.jsonc |
.json5 |
Nguyên tắc chọn rất đơn giản: luôn dùng JSON chuẩn khi đi qua ranh giới tiến trình, còn tệp cấu hình có thể dùng JSONC hoặc JSON5 tùy trình soạn thảo của bạn hỗ trợ gì.
tsconfig.json và .vscode/settings.json có thể chứa chú thích vì trình soạn thảo phân tích chúng như JSONC; nhưng đưa chúng cho một
JSON.parse nghiêm ngặt vẫn báo lỗi.
JSON có thể chứa chú thích không?
Không. Ngữ pháp trong RFC 8259 không có luật sinh nào cho chú thích, nên bất kỳ // hay /* */ nào cũng làm việc phân tích thất bại.
Nếu bạn muốn giữ phần giải thích trong cấu hình, có ba cách thay thế:
{
"_comment": "以下超时单位是毫秒",
"timeout": 3000,
"retry": {
"$doc": "重试次数,0 表示不重试",
"count": 3
}
}
- Dùng các trường quy ước như
_commentmà người đọc chỉ cần bỏ qua; - Dùng các trường có tiền tố
$để giảm khả năng trùng với trường nghiệp vụ; - Đơn giản là chuyển cấu hình sang JSONC hoặc YAML rồi chuyển về JSON chuẩn lúc chạy.
Tại sao dấu phẩy cuối lại gây lỗi?
Vì trong ngữ pháp, cả mảng lẫn object đều là cấu trúc "phần tử + dấu phân cách", và không được phép có dấu phẩy sau phần tử cuối.
{
"a": 1,
"b": 2,
}
Đoạn trên có một dấu phẩy thừa trước }, và lỗi thường được báo ở chính dấu phẩy hoặc ở dấu ngoặc đóng tùy cách hiện thực của trình phân tích.
Đây cũng là cái bẫy dễ mắc nhất khi chuyển từ JavaScript — object literal của JS cho phép dấu phẩy cuối, JSON thì không.
Có được dùng nháy đơn không?
Không. Chuỗi phải dùng nháy kép; 'text' là lỗi cú pháp.
Nếu dữ liệu được sao chép từ repr() của Python, từ một script shell, hay từ cấu hình viết tay, hãy chuyển nó bằng công cụ trước
thay vì hy vọng trình phân tích dễ tính.
Khóa có thể không có nháy không?
Không. Tên thành viên của object phải là chuỗi được bọc trong nháy kép.
{ name: "json" }
Đó là object literal của JavaScript, không phải JSON. Thêm nháy vào là hợp lệ:
{ "name": "json" }
Cấp cao nhất có thể là mảng hoặc chuỗi không?
Có. RFC 8259 cho phép bất kỳ giá trị JSON nào ở cấp cao nhất: object, mảng, chuỗi, số, true, false và null đều hợp lệ.
Tuy nhiên trong thực tiễn kỹ thuật, nên chỉ dùng object hoặc mảng ở cấp cao nhất, vì hai lý do:
- Tương thích: các trình phân tích cũ (thời RFC 4627) yêu cầu cấp cao nhất phải là object hoặc mảng;
- Bảo mật: mảng ở cấp cao nhất từng mang rủi ro JSON hijacking. Trình duyệt hiện đại đã khắc phục, nhưng các hệ thống cũ vẫn giả định theo quy ước đó.
Vậy "hello" là JSON hợp lệ và ["a", "b"] cũng vậy, nhưng một API trả về {"data": [...]} sẽ an toàn hơn.
2. Encoding và chuỗi
BOM ở đầu tệp có làm hỏng việc phân tích không?
Có. RFC 8259 quy định rõ văn bản JSON phải được truyền dưới dạng UTF-8 và không được thêm byte order mark (BOM).
BOM là ba byte EF BB BF ở ngay đầu tệp. Nó hoàn toàn vô hình, nhưng chiếm vị trí đầu tiên của chuỗi,
nên trình phân tích kết luận rằng "ký tự đầu tiên không phải {" và báo lỗi — và dòng, cột được báo thường là dòng 1, cột 1, trông rất khó hiểu.
Notepad trên Windows khi "Lưu dưới dạng UTF-8" mặc định có kèm BOM. Cách kiểm tra và loại bỏ:
# Check: output starting with efbbbf means a BOM is present
head -c 3 data.json | xxd
# Remove: skip the first 3 bytes with tail
tail -c +4 data.json > clean.json
JSON.parse của JavaScript hiện đại chấp nhận BOM (V8 bỏ qua nó ở bên trong), nhưng các hiện thực nghiêm ngặt trong Java, Go và Python thì không,
và đây là lý do phổ biến khiến "cùng một tệp cho kết quả khác nhau ở các ngôn ngữ khác nhau".
Ký tự tiếng Trung có cần chuyển thành \uXXXX không?
Không. RFC 8259 yêu cầu văn bản JSON là UTF-8, nên tiếng Trung có thể xuất hiện trực tiếp; {"name": "张三"} hoàn toàn hợp lệ.
\uXXXX chỉ là một cách viết tương đương, dùng cho những kênh không thể biểu diễn ký tự trực tiếp. Quy tắc kinh nghiệm:
- Phần thân yêu cầu HTTP, tệp, cơ sở dữ liệu: viết tiếng Trung trực tiếp, tiết kiệm 3 byte mỗi ký tự;
- HTTP header, tham số URL, nhúng bên trong một chuỗi khác: dùng escape để tránh vấn đề encoding.
Những ký tự bắt buộc phải escape chỉ là ký tự điều khiển (U+0000 đến U+001F). Ví dụ, xuống dòng phải viết là \n;
bạn không thể đặt một ký tự xuống dòng thật vào bên trong chuỗi.
Emoji có thể đi thẳng vào chuỗi không?
Có, nhưng hãy nhớ rằng nó chiếm hai đơn vị mã UTF-16.
Emoji và nhiều ký tự hiếm (như 𠮷) nằm ngoài BMP, nên trong JavaScript "😀".length === 2.
Vì thế việc kiểm chứng bằng số ký tự rất dễ sai:
const text = "😀";
console.log(text.length); // 2, number of UTF-16 code units
console.log([...text].length); // 1, number of code points
console.log(Buffer.byteLength(text)); // 4, number of UTF-8 bytes
Nếu giới hạn độ dài biệt danh được tính bằng .length, một emoji chiếm hai chỗ; nếu backend đếm byte thì là bốn.
Khi thống nhất định nghĩa, hãy nói rõ bạn đang nói về code point, code unit hay byte.
3. Số và giá trị đặc biệt
Tại sao số nguyên lớn 9007199254740993 lại thành 9007199254740992?
Vì JSON.parse của JavaScript chuyển số thành số thực dấu phẩy động chính xác kép IEEE 754, và số nguyên an toàn lớn nhất là 2^53-1.
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
9007199254740993 // actually yields 9007199254740992
Bất kỳ số nào vượt quá 2^53-1 đều có thể bị làm tròn ở các chữ số cuối. Nạn nhân điển hình là ID người dùng 19 chữ số do thuật toán snowflake sinh ra, mã đơn hàng, và timestamp nano giây.
Ba cách khắc phục, xếp theo mức ưu tiên:
- Tuần tự hóa ID thành chuỗi trên máy chủ:
"id": "9007199254740993", loại bỏ vấn đề độ chính xác từ gốc; - Dùng thư viện phân tích không mất mát: các thay thế cho
JSON.parsecó thể giữ nguyên số nguyên lớn, ví dụjson-bigint; - Trích bằng biểu thức chính quy chỉ để hiển thị: không phân tích toàn bộ tài liệu, chỉ khớp trường mục tiêu dưới dạng văn bản.
Lưu ý JSON.stringify có vấn đề đối xứng: một số nằm ngoài phạm vi số nguyên an toàn có thể không còn giữ nguyên giá trị gốc sau khi tuần tự hóa.
NaN và Infinity có thể đi vào JSON không?
Không. Luật sinh số của JSON chỉ cho phép ký hiệu thập phân, và NaN, Infinity, -Infinity không phải giá trị JSON hợp lệ.
Cách xử lý của JavaScript là "suy giảm âm thầm":
JSON.stringify({ value: NaN }); // '{"value":null}'
JSON.stringify({ value: Infinity }); // '{"value":null}'
Không báo lỗi, NaN chỉ đơn giản biến thành null. Đây là cảnh điển hình "frontend gửi NaN và backend nhận null".
Khi cần truyền đạt ngữ nghĩa như vậy, hãy biểu diễn rõ ràng bằng chuỗi hoặc giá trị sentinel đã thống nhất:
{ "ratio": null, "ratioState": "not-a-number" }
Số có thể viết là 1e3 hoặc 0.5 không?
Có. Số JSON hỗ trợ phần thập phân và số mũ, nên 1e3, 1E3, 0.5 và -0.5 đều hợp lệ.
Nhưng có những hạn chế:
- Không được bắt đầu hoặc kết thúc bằng
.:.5và5.đều không hợp lệ, hãy viết0.5và5; - Không có số 0 ở đầu:
01không hợp lệ; - Không có thập lục phân:
0x1Fkhông hợp lệ, đó là phần mở rộng của JSON5; -0hợp lệ, nhưngJSON.stringify(-0)xuất ra0, làm mất dấu.
4. Ngữ nghĩa của object
Điều gì xảy ra với khóa trùng nhau?
Hành vi do hiện thực quyết định; RFC 8259 chỉ nói tên khóa "nên" là duy nhất và không quy định điều gì xảy ra khi trùng.
Trong thực tế, đại đa số hiện thực lấy giá trị cuối cùng:
JSON.parse('{"a":1,"a":2}') // { a: 2 }
Nhưng bạn không thể dựa vào điều đó. Một số thư viện Java, encoding/json của Go và json của Python khác nhau giữa các phiên bản:
có nơi lấy giá trị đầu, có nơi báo lỗi, có nơi lấy giá trị cuối. Coi khóa trùng là lỗi là cách an toàn nhất xuyên ngôn ngữ.
Bạn không thể tìm khóa trùng bằng mắt — trong JSON đã nén, cùng một khóa có thể cách nhau hàng nghìn ký tự. Hãy dùng script:
# Extract keys that appear more than once at the same level, as a rough check
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d
undefined biến thành gì?
Tùy trường hợp, và đây là chỗ dễ sinh lỗi nhất:
JSON.stringify({ a: undefined }); // '{}' the key is dropped entirely
JSON.stringify({ a: undefined, b: 1 }); // '{"b":1}'
JSON.stringify([undefined]); // '[null]' becomes null inside arrays
JSON.stringify({ a: function () {} }); // '{}' functions are dropped too
Cùng một undefined có nghĩa là "trường biến mất" khi ở trong object và "biến thành null" khi ở trong mảng.
Nếu backend rẽ nhánh khác nhau cho "thiếu trường" và "trường là null", bạn sẽ gặp mâu thuẫn ở đây.
Khi cần biểu diễn "không có giá trị" một cách rõ ràng, hãy dùng null thay vì undefined.
Điều gì xảy ra sau khi phân tích {"__proto__": {...}}?
Bản thân JSON.parse là an toàn: __proto__ được coi là thuộc tính riêng bình thường và không thay đổi prototype.
Điều nguy hiểm là thao tác gộp sau khi phân tích:
const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch); // some implementations pollute the prototype
Cách đúng là duyệt các thuộc tính riêng và bỏ qua __proto__, constructor và prototype.
Chi tiết nằm trong Câu hỏi thường gặp về hiệu năng và bảo mật.
5. Bảng tra nhanh
| Cú pháp | Hợp lệ | Ghi chú |
|---|---|---|
{"a": 1,} |
Không | Dấu phẩy cuối |
{'a': 1} |
Không | Nháy đơn |
{a: 1} |
Không | Khóa không có nháy |
{"a": 1} // comment |
Không | Chú thích |
"hello" |
Có | Chuỗi ở cấp cao nhất hợp lệ, nhưng hiếm dùng trong API |
{"a": 1e3} |
Có | Hỗ trợ số mũ |
{"a": 01} |
Không | Số 0 ở đầu |
{"a": NaN} |
Không | Literal không hợp lệ; stringify biến nó thành null |
\uFEFF{"a":1} |
Không | BOM không phải phần của văn bản JSON |
{"a":1,"a":2} |
Tranh cãi | Hiện thực lấy giá trị cuối, nhưng đừng dựa vào đó |