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