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": "Chinese text, emoji 🎉 and full-width symbols can be written directly",
  "escaped": "A line break goes here\nand a tab\tfinishes it",
  "control": "\u0007 is the bell character and must be escaped",
  "quote": "He said: \"don't use single quotes\""
}

\/ — устаревшая escape-последовательность (она не даёт </script> закрыть блок раньше времени). В современных сценариях она нужна редко, но парсеры обязаны её поддерживать.

4. Кодировка: UTF-8 без BOM

Раздел 8.1 RFC 8259 говорит три вещи:

  1. Текст JSON, используемый для обмена, должен быть в кодировке UTF-8;
  2. Реализации не должны добавлять BOM (EF BB BF) в начало текста;
  3. Парсеры могут игнорировать BOM, чтобы быть устойчивее.

Два типичных сбоя из реальной жизни:

  • «Блокнот» Windows сохраняет файл как «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' }        // single quotes
{ name: "json" }          // key name not quoted
{"a": 1,}                 // trailing comma
[1, 2, 3,]                // trailing comma in an array
{"a": 01}                 // leading zero
{"a": +1}                 // leading plus sign
{"a": .5}                 // missing integer part
{"a": NaN}                // non-standard literal
{"a": undefined}          // not a JSON value
{"a": 1} // comment       // 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 его отвергает» почти всегда проблема диалекта. При конвертации сначала удалите комментарии, одинарные кавычки и завершающие запятые и передайте результат стандартному парсеру.