JSON Toolkit

Вопросы о синтаксисе и форматах: комментарии, висячие запятые и точность

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

Начинающий~12 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка вопросов о синтаксисе и форматах

Главное: пишите обычный 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 — все валидны.

Однако в инженерной практике рекомендуется использовать на верхнем уровне только объекты или массивы, по двум причинам:

  1. Совместимость: старые парсеры (эпохи RFC 4627) требуют, чтобы верхний уровень был объектом или массивом;
  2. Безопасность: массив верхнего уровня исторически несёт риск перехвата 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, номера заказов и наносекундные метки времени.

Три исправления, по предпочтению:

  1. Сериализуйте ID как строку на сервере: "id": "9007199254740993", устраняя проблему точности в корне;
  2. Используйте библиотеку без потерь: замены JSON.parse могут сохранять большие целые дословно, например json-bigint;
  3. Извлекайте регулярным выражением только для отображения: не парсите весь документ, а сопоставляйте целевое поле как текст.

Учтите, что 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} Спорно Реализации берут последнее значение, но не полагайтесь на это