Вопросы о синтаксисе и форматах: комментарии, висячие запятые и точность
Главное: Разрешены ли комментарии, почему висячая запятая даёт ошибку, нужно ли брать ключи в кавычки, почему 9007199254740993 меняется — двенадцать выводов, каждый проверен по RFC 8259.

Главное: пишите обычный 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, shell-скрипта или написанной вручную конфигурации, сначала конвертируйте их инструментом,
а не надейтесь, что парсер снисходителен.
Могут ли ключи быть без кавычек?
Нет. Имена членов объекта должны быть строками в двойных кавычках.
{ name: "json" }
Это литерал объекта JavaScript, а не JSON. Добавление кавычек делает его валидным:
{ "name": "json" }
Может ли верхний уровень быть массивом или строкой?
Да. RFC 8259 допускает любое значение JSON на верхнем уровне: объекты, массивы, строки, числа, true, false и null — все валидны.
Однако в инженерной практике рекомендуется использовать на верхнем уровне только объекты или массивы, по двум причинам:
- Совместимость: старые парсеры (эпохи RFC 4627) требуют, чтобы верхний уровень был объектом или массивом;
- Безопасность: массив верхнего уровня исторически несёт риск перехвата JSON. Современные браузеры это исправили, но старые системы всё ещё предполагают соглашение.
Итак, "hello" — валидный JSON, ["a", "b"] тоже, но API, возвращающий {"data": [...]}, безопаснее.
2. Кодировка и строки
Ломает ли BOM в начале файла парсинг?
Да. RFC 8259 явно требует передавать текст JSON как UTF-8 и не должен добавлять метку порядка байтов (BOM).
BOM — это три байта EF BB BF в самом начале файла. Он совершенно невидим, но занимает первую позицию строки,
поэтому парсер заключает, что «первый символ не {», и выдаёт ошибку — а сообщаемая строка и столбец обычно строка 1, столбец 1, что выглядит обескураживающе.
«Сохранить как UTF-8» в Блокноте Windows по умолчанию включает 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 пользователей, генерируемые snowflake, номера заказов и наносекундные метки времени.
Три исправления, по предпочтению:
- Сериализуйте ID как строку на сервере:
"id": "9007199254740993", устраняя проблему точности в корне; - Используйте библиотеку без потерь: замены
JSON.parseмогут сохранять большие целые дословно, напримерjson-bigint; - Извлекайте регулярным выражением только для отображения: не парсите весь документ, а сопоставляйте целевое поле как текст.
Учтите, что 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} |
Спорно | Реализации берут последнее значение, но не полагайтесь на это |