JSON Toolkit

Часті питання про синтаксис і формати: коментарі, висячі коми й точність

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

Початковий~12 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Обкладинка частих питань про синтаксис і формати

Коротко: пишіть простий 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 усі валідні.

В інженерній практиці, однак, рекомендується використовувати на верхньому рівні лише обʼєкти або масиви, з двох причин:

  1. Сумісність: старіші парсери (з епохи RFC 4627) вимагають, щоб верхній рівень був обʼєктом або масивом;
  2. Безпека: верхньорівневий масив історично ніс ризик викрадення 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 користувачів, згенеровані алгоритмами-сніжинками, номери замовлень і наносекундні часові мітки.

Три виправлення, за перевагою:

  1. Серіалізуйте ID як рядок на сервері: "id": "9007199254740993", усуваючи проблему точності в корені;
  2. Використовуйте бібліотеку розбору без втрат: заміни JSON.parse можуть зберігати великі цілі дослівно, наприклад json-bigint;
  3. Видобувайте регулярним виразом лише для показу: не розбирайте весь документ, просто зіставте цільове поле як текст.

Зауважте, що 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} Спірно Реалізації беруть останнє значення, але не покладайтеся на це