Syntax and Format FAQ: Comments, Trailing Commas, and Precision
Bottom line: Whether comments are allowed, why a trailing comma errors, whether keys need quotes, why 9007199254740993 changes — twelve conclusions, each checked against RFC 8259.

Bottom line: write plain RFC 8259 JSON — double quotes, no comments, no trailing commas, UTF-8 without a BOM — for anything crossing a process boundary; use JSONC or JSON5 for configuration that needs comments and convert back at runtime; and serialize integers above 2^53-1 as strings on the server.
What these questions have in common: they look like they "should work", but the spec is actually very strict.
1. What counts as valid JSON
What is the difference between JSON, JSON5, and JSONC?
Standard JSON recognizes only double-quoted strings and allows neither comments nor trailing commas. JSON5 and JSONC both add features on top of it:
| Feature | JSON (RFC 8259) | JSONC | JSON5 |
|---|---|---|---|
| Double-quoted strings | Supported | Supported | Supported |
| Single-quoted strings | Not supported | Not supported | Supported |
| Comments | Not supported | Supported | Supported |
| Trailing commas | Not supported | Not supported | Supported |
| Unquoted keys | Not supported | Not supported | Supported |
| Hexadecimal numbers | Not supported | Not supported | Supported |
NaN / Infinity |
Not supported | Not supported | Supported |
| File extension | .json |
.jsonc |
.json5 |
The selection principle is simple: always use standard JSON across process boundaries, while configuration files may use JSONC or JSON5 depending on what your editor supports.
tsconfig.json and .vscode/settings.json can contain comments because the editor parses them as JSONC; feeding them to a strict
JSON.parse still errors.
Can JSON contain comments?
No. The grammar in RFC 8259 has no comment production, so any // or /* */ makes parsing fail.
If you want to keep explanations in configuration, there are three alternatives:
{
"_comment": "以下超时单位是毫秒",
"timeout": 3000,
"retry": {
"$doc": "重试次数,0 表示不重试",
"count": 3
}
}
- Use conventional fields such as
_commentthat the reader simply ignores; - Use
$-prefixed fields to reduce the chance of colliding with business fields; - Simply switch to JSONC or YAML for configuration and convert to standard JSON at runtime.
Why does a trailing comma cause an error?
Because in the grammar both arrays and objects are "element + separator" structures, and no comma is allowed after the last element.
{
"a": 1,
"b": 2,
}
The snippet above has an extra comma before }, and the error is usually reported at the comma or at the closing brace depending on the parser implementation.
This is also the easiest trap when migrating from JavaScript — JS object literals allow trailing commas, JSON does not.
Are single quotes allowed?
No. Strings must use double quotes; 'text' is a syntax error.
If the data was copied from Python's repr(), a shell script, or hand-written configuration, convert it with a tool first
rather than hoping the parser is lenient.
Can keys be unquoted?
No. Object member names must be strings wrapped in double quotes.
{ name: "json" }
That is a JavaScript object literal, not JSON. Adding quotes makes it valid:
{ "name": "json" }
Can the top level be an array or a string?
Yes. RFC 8259 allows any JSON value at the top level: objects, arrays, strings, numbers, true, false, and null are all valid.
In engineering practice, however, it is recommended to use only objects or arrays at the top level, for two reasons:
- Compatibility: older parsers (from the RFC 4627 era) require the top level to be an object or an array;
- Security: a top-level array has historically carried a JSON hijacking risk. Modern browsers have fixed it, but older systems still assume the convention.
So "hello" is valid JSON and ["a", "b"] is too, but an API returning {"data": [...]} is safer.
2. Encoding and strings
Does a BOM at the start of the file break parsing?
Yes. RFC 8259 explicitly requires JSON text to be transmitted as UTF-8 and must not add a byte order mark (BOM).
The BOM is the three bytes EF BB BF at the very start of the file. It is completely invisible, but it occupies the first position of the string,
so the parser concludes that "the first character is not {" and errors — and the reported line and column are usually line 1, column 1, which looks baffling.
Windows Notepad's "Save as UTF-8" includes a BOM by default. To check and remove it:
# 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
Modern JavaScript's JSON.parse tolerates a BOM (V8 skips it internally), but strict implementations in Java, Go, and Python do not,
which is a common reason why "the same file gives different results in different languages".
Do Chinese characters need to be converted to \uXXXX?
No. RFC 8259 requires JSON text to be UTF-8, so Chinese can appear directly; {"name": "张三"} is perfectly valid.
\uXXXX is merely an equivalent alternative spelling, used for channels that cannot represent the character directly. The rule of thumb:
- HTTP request bodies, files, databases: write Chinese directly, saving 3 bytes per character;
- HTTP headers, URL parameters, embedding inside another string: use escapes to avoid encoding issues.
The only characters that must be escaped are control characters (U+0000 through U+001F). For example, a newline must be written as \n;
you cannot put a literal newline inside a string.
Can emoji go directly into a string?
Yes, but remember it takes two UTF-16 code units.
Emoji and many rare characters (such as 𠮷) fall outside the BMP, so in JavaScript "😀".length === 2.
Validating by character count is therefore easy to get wrong:
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
If a nickname length limit is computed with .length, one emoji consumes two slots; if the backend counts bytes, it is four.
When aligning definitions, state clearly whether you mean code points, code units, or bytes.
3. Numbers and special values
Why did the big integer 9007199254740993 become 9007199254740992?
Because JavaScript's JSON.parse converts numbers into IEEE 754 double-precision floats, and the largest safe integer is 2^53-1.
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
9007199254740993 // actually yields 9007199254740992
Any number above 2^53-1 may be rounded in its final digits. Typical victims are 19-digit user IDs generated by snowflake algorithms, order numbers, and nanosecond timestamps.
Three fixes, ordered by preference:
- Serialize the ID as a string on the server:
"id": "9007199254740993", removing the precision problem at its root; - Use a lossless parsing library: replacements for
JSON.parsecan preserve big integers verbatim, for examplejson-bigint; - Extract with a regular expression for display only: do not parse the whole document, just match the target field as text.
Note that JSON.stringify has the symmetric problem: a number outside the safe integer range may no longer hold its original value after serialization.
Can NaN and Infinity go into JSON?
No. The JSON number production allows only decimal notation, and NaN, Infinity, and -Infinity are not valid JSON values.
JavaScript's approach is "silent degradation":
JSON.stringify({ value: NaN }); // '{"value":null}'
JSON.stringify({ value: Infinity }); // '{"value":null}'
No error, NaN simply becomes null. This is the classic scene where "the frontend sent NaN and the backend received null".
When you need to convey such semantics, express them explicitly with strings or agreed sentinel values:
{ "ratio": null, "ratioState": "not-a-number" }
Can numbers be written as 1e3 or 0.5?
Yes. JSON numbers support decimals and exponents, so 1e3, 1E3, 0.5, and -0.5 are all valid.
But there are restrictions:
- They cannot start or end with
.:.5and5.are both invalid, so write0.5and5; - No leading zeros:
01is invalid; - No hexadecimal:
0x1Fis invalid, that is a JSON5 extension; -0is valid, butJSON.stringify(-0)outputs0, losing the sign.
4. Object semantics
What happens with duplicate keys?
Behavior is implementation-defined; RFC 8259 only says key names "should" be unique and does not specify what happens on duplicates.
In practice the vast majority of implementations take the last value:
JSON.parse('{"a":1,"a":2}') // { a: 2 }
But you cannot rely on it. Some Java libraries, Go's encoding/json, and Python's json differ across versions:
some take the first value, some error out, and some take the last. Treating duplicate keys as an error is the safest approach across languages.
You cannot find duplicate keys by eye — in minified JSON the same key may be thousands of characters apart. Use a 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
What does undefined become?
It depends, and this is the most bug-prone spot:
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
The same undefined means "the field disappears" inside an object and "becomes null" inside an array.
If the backend takes different branches for "field missing" and "field is null", you get an inconsistency here.
When you need to express "no value" explicitly, use null rather than undefined.
What happens after parsing {"__proto__": {...}}?
JSON.parse itself is safe: __proto__ is treated as an ordinary own property and does not change the prototype.
What is dangerous is the merge operation after parsing:
const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch); // some implementations pollute the prototype
The correct approach is to iterate own properties and skip __proto__, constructor, and prototype.
Details are in the Performance and Security FAQ.
5. Quick reference
| Syntax | Valid | Notes |
|---|---|---|
{"a": 1,} |
No | Trailing comma |
{'a': 1} |
No | Single quotes |
{a: 1} |
No | Key without quotes |
{"a": 1} // comment |
No | Comments |
"hello" |
Yes | A top-level string is valid, but rarely used in APIs |
{"a": 1e3} |
Yes | Exponents are supported |
{"a": 01} |
No | Leading zero |
{"a": NaN} |
No | Invalid literal; stringify turns it into null |
\uFEFF{"a":1} |
No | A BOM is not part of JSON text |
{"a":1,"a":2} |
Disputed | Implementations take the last value, but do not rely on it |