JSON Toolkit

Читаємо межі граматики JSON за RFC 8259

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

Початковий~11 хв читанняОпубліковано 2026-10-30Оновлено 2026-10-30
Обкладинка статті «Читаємо межі граматики JSON за 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 ) ] "]"

Є лише шість ключових моментів:

  1. Об'єкти використовують фігурні дужки, члени розділяються комами, а кома після останнього члена не допускається;
  2. Масиви використовують квадратні дужки, елементи розділяються комами, висячі коми так само не допускаються;
  3. Числа не допускають провідного +, провідних нулів (01 невалідне), голої десяткової точки (1. невалідне), NaN чи Infinity;
  4. Рядки мають використовувати подвійні лапки; одинарні лапки невалідні;
  5. Булеві значення та null — літерали в нижньому регістрі; True і NULL обидва невалідні;
  6. Між структурними елементами можуть стояти лише пробільні символи 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 каже три речі:

  1. Текст JSON, що використовується для обміну, мусить бути в кодуванні UTF-8;
  2. Реалізації не повинні додавати BOM (EF BB BF) на початок тексту;
  3. Парсери можуть ігнорувати 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 його відхиляє» майже завжди є проблемою діалекту. Під час конвертації спершу видаліть коментарі, одинарні лапки й висячі коми, а результат передайте стандартному парсеру.