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

Коротко: коли з'являється синтаксична помилка JSON, знайдіть перший недопустимий символ і класифікуйте його як одинарну лапку, висячу кому, коментар чи керівний символ, а не перечитуйте весь документ; щодо кодування переконайтеся, що це UTF-8 без BOM, а цілі числа, більші за 2^53 - 1, передавайте рядками.
Тіло специфікації займає лише десяток сторінок, але за кожним «MUST», «SHOULD» і «MAY» ховається інцидент, через який колись команда не спала всю ніч.
1. Як співвідносяться три тексти
Вивчення JSON часто заплутується через три номери: RFC 4627, RFC 7159, RFC 8259 і ще ECMA-404. Насправді зв'язок досить ясний:
| Текст | Дата | Статус | Ключова відмінність |
|---|---|---|---|
| RFC 4627 | 2006 | Застарілий | Верхній рівень має бути об'єктом або масивом |
| RFC 7159 | 2014 | Застарілий | Верхній рівень послаблено до будь-якого значення JSON |
| RFC 8259 | 2017.12 | Чинний стандарт | Явний UTF-8, додано настанови щодо сумісності |
| ECMA-404 | 2013, друга редакція 2017.12 | Специфікація граматики | Визначає лише граматику, не охоплює кодування й передачу |
Який читати? Достатньо RFC 8259. ECMA-404 еквівалентний у частині граматики; різниця в тому, що RFC 8259 ще й визначає «передачу як UTF-8» і «проблеми сумісності, за якими мають стежити реалізації».
2. Шість типів значень
Граматичні продукції специфікації можна стиснути в одну схему:
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
Є лише шість ключових моментів:
- Об'єкти використовують фігурні дужки, члени розділяються комами, а кома після останнього члена не допускається;
- Масиви використовують квадратні дужки, елементи розділяються комами, висячі коми так само не допускаються;
- Числа не допускають провідного
+, провідних нулів (01невалідне), голої десяткової точки (1.невалідне),NaNчиInfinity; - Рядки мають використовувати подвійні лапки; одинарні лапки невалідні;
- Булеві значення та null — літерали в нижньому регістрі;
TrueіNULLобидва невалідні; - Між структурними елементами можуть стояти лише пробільні символи
0x20(пробіл),0x09(табуляція),0x0A(переведення рядка) і0x0D(повернення каретки).
Наведений нижче фрагмент є валідним JSON, хоча багато хто, побачивши його вперше, вважає його помилковим — бо це голий рядок верхнього рівня:
"just a string"
Від RFC 7159 верхній рівень справді може бути будь-яким значенням. В API, утім, усе ще рекомендується використовувати лише об'єкти або масиви; причина — у розділі 6.
3. Що насправді можна писати безпосередньо в рядку
Саме тут найлегше обдуритися фразою «у мене все працює». Правила специфікації для рядків такі:
- Символи, дозволені безпосередньо:
%x20-21,%x23-5B,%x5D-10FFFF; - Інакше кажучи, подвійну лапку
"(0x22) і зворотну скісну риску\(0x5C) треба екранувати; - Усі керівні символи нижче 0x20 треба екранувати, включно з переведенням рядка, табуляцією та поверненням каретки;
- Доступні лише escape-послідовності
\",\\,\/,\b,\f,\n,\r,\tта\uXXXX.
{
"ok": "Китайський текст, емодзі 🎉 і повноширинні символи можна писати безпосередньо",
"escaped": "Тут буде переведення рядка\nа табуляція\tзавершує його",
"control": "\u0007 — це символ дзвінка, його треба екранувати",
"quote": "Він сказав: \"не використовуйте одинарні лапки\""
}
\/ — застарілий escape (він заважає </script> передчасно закрити тег). Сучасні сценарії рідко його потребують, але парсери зобов'язані його підтримувати.
4. Кодування: UTF-8, без BOM
Розділ 8.1 RFC 8259 каже три речі:
- Текст JSON, що використовується для обміну, мусить бути в кодуванні UTF-8;
- Реалізації не повинні додавати BOM (
EF BB BF) на початок тексту; - Парсери можуть ігнорувати BOM, щоб бути стійкішими.
Два типові збої в реальному світі:
- Windows Notepad зберігає як «UTF-8 з BOM», і файл відхиляється
JSON.parseзUnexpected token; - Китайський файл JSON, збережений у GBK, перетворюється на «кракозябри» на конвеєрі, що декодує лише як UTF-8, але синтаксис при цьому випадково проходить, тож помилка відкладається до бізнес-рівня.
Обробка проста: видаліть BOM під час читання файлу, потім явно декодуйте як UTF-8; під час запису переконайтеся, що BOM не додано.
/**
* Removes the UTF-8 BOM at the start of a string.
* @param text The original text
* @returns The text without a BOM
*/
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
Замітка про медіатипи, поки ми тут: application/json більше не приймає параметра charset; його написання не спричиняє помилки, але це застаріла практика.
5. Три місця, де специфікація свідомо залишає простір
Саме там, де специфікація відмовляється щось жорстко закріпити, і виникають відмінності реалізацій.
1. Діапазон і точність чисел
RFC 8259 лише каже, що «ця специфікація не обмежує діапазон чи точність чисел», зауважуючи, що більшість реалізацій використовують подвійну точність IEEE 754.
Це означає, що те саме 9223372036854775807 у JavaScript стає 9223372036854776000, а в Java long залишається точним.
Висновок: цілі числа, більші за 2^53 - 1, і грошові суми, що потребують багатьох знаків після коми, слід завжди передавати рядками, а pattern явно записувати в схемі.
2. Повторювані ключі
Специфікація формулює: «імена в межах об'єкта бажано (SHOULD) були унікальними»; вона не каже, що робити, коли це не так.
JSON.parseзберігає останній;- Деякі парсери зберігають перший;
- Деякі бібліотеки просто кидають помилку.
Висновок: не пишіть повторюваних ключів і не покладайтеся на жодну конкретну поведінку у валідаторах. Додати крок «виявлення повторюваних ключів» під час збірки значно дешевше, ніж розслідувати потім.
3. Обмеження глибини й довжини
Специфікація не встановлює межі глибини вкладеності, але кожна реалізація її має. V8 кидає RangeError: Maximum call stack size exceeded на десятках тисяч рівнів вкладеності.
Сервери мають накладати власні обмеження; це вимога безпеки, а не специфікації.
6. Перевірте розуміння на контрприкладах
Кожен рядок нижче — невалідний JSON. Подайте їх парсеру й подивіться, чи корисні повідомлення про помилки:
{ 'name': 'json' } // одинарні лапки
{ name: "json" } // ключ без лапок
{"a": 1,} // висяча кома
[1, 2, 3,] // висяча кома в масиві
{"a": 01} // провідний нуль
{"a": +1} // провідний плюс
{"a": .5} // відсутня ціла частина
{"a": NaN} // нестандартний літерал
{"a": undefined} // не значення JSON
{"a": 1} // comment // коментар
Цікавий граничний випадок: {"a": 1} з коментарем після нього валідний у JSON5, валідний у JSONC і невалідний у стандартному JSON.
Тож «файл конфігурації працює, а API його відхиляє» — майже завжди проблема діалекту.
7. Виробничий чек-лист
- [ ] Увесь JSON, що передається назовні, використовує
application/jsonбезcharset; - [ ] Конвеєри читання й запису файлів видаляють BOM і використовують явний UTF-8;
- [ ] Цілі числа, більші за
2^53, передаються рядками й анотуються в схемі; - [ ] Повторювані ключі виявляються під час збірки;
- [ ] Сервер обмежує розмір тіла запиту й глибину вкладеності;
- [ ] Значення верхнього рівня публічних API — об'єкти або масиви, ніколи голі скаляри.
Часті питання
Як швидко визначити, чи є фрагмент тексту валідним JSON?
Передайте текст парсеру або онлайн-інструменту розбору, прочитайте позицію першого недопустимого символу, потім звірте її з шістьма типами значень і правилами пробільних символів RFC 8259. Верхній рівень може бути будь-яким значенням JSON, але між структурними елементами дозволені лише пробіл, табуляція, переведення рядка й повернення каретки. Одинарна лапка, ключ без лапок, висяча кома або коментар означають, що це не стандартний JSON.
Чому JSON має використовувати UTF-8 і не містити BOM?
Розділ 8.1 RFC 8259 вимагає, щоб текст JSON для обміну був у кодуванні UTF-8, каже, що реалізації не повинні додавати BOM на початку, і дозволяє парсерам ігнорувати BOM заради стійкості. Файл із BOM відхиляється JSON.parse з Unexpected token, хоча оком нічого не видно. Тож видаляйте BOM під час читання й декодуйте явно як UTF-8.
Чи варто використовувати JSON5 або JSONC, щоб обійти ці межі граматики?
Так, але лише для локального читання, як-от файли конфігурації; дані, що обмінюються між системами, мають залишатися стандартним JSON. Контрприклади показують, що {"a": 1} з коментарем після нього валідний у JSON5 і JSONC, але невалідний у стандартному JSON, тому «файл конфігурації працює, а API його відхиляє» майже завжди є проблемою діалекту. Під час конвертації спершу видаліть коментарі, одинарні лапки й висячі коми, а результат передайте стандартному парсеру.