Часті питання про синтаксис і формати: коментарі, висячі коми й точність
Головне: Чи дозволені коментарі, чому висяча кома дає помилку, чи потрібні лапки ключам, чому 9007199254740993 змінюється — дванадцять висновків, кожен звірено з RFC 8259.

Коротко: пишіть простий JSON RFC 8259 — подвійні лапки, без коментарів, без висячих ком, UTF-8 без BOM — для всього, що перетинає межу процесу; використовуйте JSONC або JSON5 для конфігурації, що потребує коментарів, і конвертуйте назад під час виконання; і серіалізуйте цілі числа понад 2^53-1 як рядки на сервері.
Що спільного в цих питаннях: вони виглядають так, ніби «мали б працювати», але специфікація насправді вельми строга.
1. Що вважається валідним JSON
Яка різниця між JSON, JSON5 і JSONC?
Стандартний JSON визнає лише рядки в подвійних лапках і не дозволяє ні коментарів, ні висячих ком. JSON5 і JSONC обидва додають можливості поверх нього:
| Можливість | JSON (RFC 8259) | JSONC | JSON5 |
|---|---|---|---|
| Рядки в подвійних лапках | Підтримуються | Підтримуються | Підтримуються |
| Рядки в одинарних лапках | Не підтримуються | Не підтримуються | Підтримуються |
| Коментарі | Не підтримуються | Підтримуються | Підтримуються |
| Висячі коми | Не підтримуються | Не підтримуються | Підтримуються |
| Ключі без лапок | Не підтримуються | Не підтримуються | Підтримуються |
| Шістнадцяткові числа | Не підтримуються | Не підтримуються | Підтримуються |
NaN / Infinity |
Не підтримуються | Не підтримуються | Підтримуються |
| Розширення файлу | .json |
.jsonc |
.json5 |
Принцип вибору простий: завжди використовуйте стандартний JSON через межі процесів, тоді як конфігураційні файли можуть використовувати JSONC або JSON5 залежно від того, що підтримує ваш редактор.
tsconfig.json і .vscode/settings.json можуть містити коментарі, бо редактор розбирає їх як JSONC; подача їх строгому
JSON.parse усе одно дає помилку.
Чи може JSON містити коментарі?
Ні. Граматика RFC 8259 не має продукції коментаря, тож будь-який // або /* */ робить розбір збійним.
Якщо хочете тримати пояснення в конфігурації, є три альтернативи:
{
"_comment": "以下超时单位是毫秒",
"timeout": 3000,
"retry": {
"$doc": "重试次数,0 表示不重试",
"count": 3
}
}
- Використовуйте конвенційні поля на кшталт
_comment, які читач просто ігнорує; - Використовуйте поля з префіксом
$, щоб зменшити шанс зіткнення з бізнес-полями; - Просто перемкніться на JSONC або YAML для конфігурації й конвертуйте в стандартний JSON під час виконання.
Чому висяча кома спричиняє помилку?
Бо в граматиці і масиви, і обʼєкти — структури «елемент + роздільник», і кома після останнього елемента не дозволена.
{
"a": 1,
"b": 2,
}
Наведений фрагмент має зайву кому перед }, і помилка зазвичай повідомляється на комі або на закривній дужці залежно від реалізації парсера.
Це також найлегша пастка під час міграції з JavaScript — обʼєктні літерали JS дозволяють висячі коми, JSON — ні.
Чи дозволені одинарні лапки?
Ні. Рядки мусять використовувати подвійні лапки; 'text' — синтаксична помилка.
Якщо дані копійовані з repr() Python, скрипта оболонки чи рукописної конфігурації, спершу конвертуйте їх інструментом,
а не сподівайтеся, що парсер поблажливий.
Чи можуть ключі бути без лапок?
Ні. Імена членів обʼєкта мусять бути рядками в подвійних лапках.
{ name: "json" }
Це обʼєктний літерал JavaScript, а не JSON. Додавання лапок робить його валідним:
{ "name": "json" }
Чи може верхній рівень бути масивом або рядком?
Так. RFC 8259 дозволяє будь-яке значення JSON на верхньому рівні: обʼєкти, масиви, рядки, числа, true, false і null усі валідні.
В інженерній практиці, однак, рекомендується використовувати на верхньому рівні лише обʼєкти або масиви, з двох причин:
- Сумісність: старіші парсери (з епохи RFC 4627) вимагають, щоб верхній рівень був обʼєктом або масивом;
- Безпека: верхньорівневий масив історично ніс ризик викрадення JSON. Сучасні браузери це виправили, але старіші системи все ще припускають конвенцію.
Тож "hello" — валідний JSON, як і ["a", "b"], але API, що повертає {"data": [...]}, безпечніше.
2. Кодування й рядки
Чи ламає BOM на початку файлу розбір?
Так. RFC 8259 явно вимагає, щоб текст JSON передавався як UTF-8 і не повинен додавати маркер порядку байтів (BOM).
BOM — це три байти EF BB BF на самому початку файлу. Він цілком невидимий, але займає першу позицію рядка,
тож парсер робить висновок, що «перший символ не {», і дає помилку — а повідомлені рядок і стовпець зазвичай рядок 1, стовпець 1, що виглядає спантеличливо.
«Зберегти як UTF-8» у Windows Notepad типово включає BOM. Щоб перевірити й видалити:
# Перевірка: вивід, що починається з efbbbf, означає наявність BOM
head -c 3 data.json | xxd
# Видалення: пропустіть перші 3 байти через tail
tail -c +4 data.json > clean.json
JSON.parse сучасного JavaScript терпить BOM (V8 внутрішньо його пропускає), але строгі реалізації в Java, Go й Python — ні,
що є поширеною причиною того, чому «той самий файл дає різні результати в різних мовах».
Чи треба конвертувати китайські символи в \uXXXX?
Ні. RFC 8259 вимагає, щоб текст JSON був UTF-8, тож китайський текст може з'являтися безпосередньо; {"name": "张三"} цілком валідний.
\uXXXX — лише еквівалентне альтернативне написання, що використовується для каналів, які не можуть подати символ безпосередньо. Практичне правило:
- Тіла HTTP-запитів, файли, бази даних: пишіть китайський текст безпосередньо, заощаджуючи 3 байти на символ;
- Заголовки HTTP, параметри URL, вкладення в інший рядок: використовуйте escape, щоб уникнути проблем кодування.
Єдині символи, які мусять бути екрановані, — керівні символи (U+0000–U+001F). Наприклад, переведення рядка треба писати як \n;
не можна вставляти буквальне переведення рядка всередині рядка.
Чи можуть емодзі йти прямо в рядок?
Так, але памʼятайте, що вони займають дві кодові одиниці UTF-16.
Емодзі й багато рідкісних символів (як-от 𠮷) лежать поза BMP, тож у JavaScript "😀".length === 2.
Перевірка за кількістю символів тому легко помиляється:
const text = "😀";
console.log(text.length); // 2, кількість кодових одиниць UTF-16
console.log([...text].length); // 1, кількість кодових точок
console.log(Buffer.byteLength(text)); // 4, кількість байтів UTF-8
Якщо ліміт довжини псевдоніма обчислюється через .length, одне емодзі споживає два слоти; якщо бекенд рахує байти — чотири.
Узгоджуючи визначення, чітко заявляйте, чи маєте на увазі кодові точки, кодові одиниці чи байти.
3. Числа й особливі значення
Чому велике ціле 9007199254740993 стало 9007199254740992?
Бо JSON.parse у JavaScript перетворює числа на числа з подвійною точністю IEEE 754, а найбільше безпечне ціле — 2^53-1.
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
9007199254740993 // фактично дає 9007199254740992
Будь-яке число понад 2^53-1 може бути округлене в останніх цифрах. Типові жертви — 19-значні ID користувачів, згенеровані алгоритмами-сніжинками, номери замовлень і наносекундні часові мітки.
Три виправлення, за перевагою:
- Серіалізуйте ID як рядок на сервері:
"id": "9007199254740993", усуваючи проблему точності в корені; - Використовуйте бібліотеку розбору без втрат: заміни
JSON.parseможуть зберігати великі цілі дослівно, наприкладjson-bigint; - Видобувайте регулярним виразом лише для показу: не розбирайте весь документ, просто зіставте цільове поле як текст.
Зауважте, що JSON.stringify має симетричну проблему: число поза діапазоном безпечних цілих може більше не тримати оригінальне значення після серіалізації.
Чи можуть NaN та Infinity йти в JSON?
Ні. Продукція числа JSON дозволяє лише десяткове написання, а NaN, Infinity і -Infinity не є валідними значеннями JSON.
Підхід JavaScript — «тихе погіршення»:
JSON.stringify({ value: NaN }); // '{"value":null}'
JSON.stringify({ value: Infinity }); // '{"value":null}'
Без помилки, NaN просто стає null. Це класична сцена «фронтенд надіслав NaN, а бекенд отримав null».
Коли треба передати таку семантику, виражайте її явно рядками або узгодженими сторожовими значеннями:
{ "ratio": null, "ratioState": "not-a-number" }
Чи можуть числа писатися як 1e3 або 0.5?
Так. Числа JSON підтримують десяткові й експоненти, тож 1e3, 1E3, 0.5 і -0.5 усі валідні.
Але є обмеження:
- Вони не можуть починатися чи закінчуватися
.:.5і5.обидва невалідні, тож пишіть0.5і5; - Без провідних нулів:
01невалідне; - Без шістнадцяткових:
0x1Fневалідне, це розширення JSON5; -0валідне, алеJSON.stringify(-0)видає0, втрачаючи знак.
4. Семантика обʼєктів
Що відбувається з повторюваними ключами?
Поведінка визначена реалізацією; RFC 8259 лише каже, що назви ключів «бажано» були унікальними, і не визначає, що відбувається з повторюваними.
На практиці переважна більшість реалізацій бере останнє значення:
JSON.parse('{"a":1,"a":2}') // { a: 2 }
Але покладатися на це не можна. Деякі бібліотеки Java, encoding/json у Go й json у Python різняться між версіями:
деякі беруть перше значення, деякі дають помилку, а деякі — останнє. Трактування повторюваних ключів як помилки — найбезпечніший підхід між мовами.
Повторювані ключі не знайти очима — у мініфікованому JSON той самий ключ може бути за тисячі символів. Використовуйте скрипт:
# Вилучіть ключі, що з'являються більше одного разу на тому самому рівні, як приблизна перевірка
jq -r 'paths(scalars) | join(".")' data.json | sort | uniq -d
Що стається з undefined?
Залежить, і це найсхильніше до багів місце:
JSON.stringify({ a: undefined }); // '{}' ключ відкидається повністю
JSON.stringify({ a: undefined, b: 1 }); // '{"b":1}'
JSON.stringify([undefined]); // '[null]' стає null усередині масивів
JSON.stringify({ a: function () {} }); // '{}' функції теж відкидаються
Той самий undefined означає «поле зникає» всередині обʼєкта й «стає null» усередині масиву.
Якщо бекенд бере різні гілки для «поле відсутнє» й «поле null», ви отримуєте неузгодженість тут.
Коли треба виразити «значення немає» явно, використовуйте null, а не undefined.
Що відбувається після розбору {"__proto__": {...}}?
Сам JSON.parse безпечний: __proto__ трактується як звичайна власна властивість і не змінює прототип.
Небезпечна операція злиття після розбору:
const patch = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign({}, patch); // деякі реалізації забруднюють прототип
Правильний підхід — обходити власні властивості й пропускати __proto__, constructor і prototype.
Деталі в Частих питаннях про продуктивність і безпеку.
5. Стислий довідник
| Синтаксис | Валідний | Примітки |
|---|---|---|
{"a": 1,} |
Ні | Висяча кома |
{'a': 1} |
Ні | Одинарні лапки |
{a: 1} |
Ні | Ключ без лапок |
{"a": 1} // comment |
Ні | Коментарі |
"hello" |
Так | Верхньорівневий рядок валідний, але редко використовується в API |
{"a": 1e3} |
Так | Експоненти підтримуються |
{"a": 01} |
Ні | Провідний нуль |
{"a": NaN} |
Ні | Невалідний літерал; stringify перетворює його на null |
\uFEFF{"a":1} |
Ні | BOM не є частиною тексту JSON |
{"a":1,"a":2} |
Спірно | Реалізації беруть останнє значення, але не покладайтеся на це |