Сразу вывод: ошибка JSON.parse() обычно не значит «модель не умеет писать JSON». Значит, вы скормили парсеру весь чат-ответ — а он принимает одно JSON-значение.JSON.parse принимает ровно одно значение по грамматике JSON (ECMA-262 / RFC 8259). Markdown-ограждения, обёрточная проза, висячие запятые, сырые переносы, обрыв и JS/Python-диалект сразу бросают SyntaxError. Порядок фикса в 2026: если можно включить Structured Output или читать поле arguments у Tool Calling — не парсите чат-прозу; если парсить всё же надо — сначала извлеките, затем parse, затем проверьте JSON Schema. Не начинайте с того, чтобы регулярками «дочинить до почти parse».
Текст актуален на 17 сентября 2026. На сайте уже есть что такое Structured Output, как ИИ генерирует JSON по JSON Schema, OpenAI vs Gemini Structured Output, структурированный JSON через Gemini API и почему Tool Calling зависит от JSON Schema. Этот текст отвечает только: почему вывод модели не проходит JSON.parse и в каком порядке это чинить.
Что JSON.parse на самом деле принимает
В браузере и Node JSON.parse реализует JSON-текст, а не «похожий на объектный литерал JavaScript». Пробельные символы (пробел, таб, перевод строки, возврат каретки) могут стоять вокруг значения. Иначе вход — ровно одно значение: объект, массив, строка, число, true / false / null. Непробельный символ после значения — отказ; Chrome часто пишет Unexpected non-whitespace character after JSON.
Эти формы живут в JS и умирают в JSON. Модели постоянно копируют их из обучающих данных:
| Форма | JS-объект / JSON5 | JSON.parse |
|---|---|---|
| Висячая запятая | {"ok": true,} можно | бросает |
| Одинарные кавычки | {'ok': true} можно | бросает |
| Комментарии | // note можно | бросает |
| Голые ключи | {ok: true} можно | бросает |
undefined / NaN / Infinity | есть в языке | бросает |
| Сырой перенос в строке | шаблонные строки позволяют | бросает; нужно \n |
Отладка с одного вопроса: вы передали «одно JSON-значение» или «абзац, которым модель обернула ответ для читаемости»? Парсеру принадлежит первое. Второе вы должны извлечь.
Таблица классов отказов
Сначала классифицируйте SyntaxError, не боритесь с точной формулировкой. Chrome, Safari и Node описывают один и тот же баг по-разному. Классов мало:
| Класс | Что модель часто эмитит | Типичный итог | Сделайте сначала |
|---|---|---|---|
| Обёртка | ограждения ```json, «вот JSON» | Первый символ не { / [ | Снять ограждение, затем вырезать сбалансированное значение |
| Диалект | Висячие запятые, одинарные кавычки, комментарии, голые ключи | Unexpected token | Перейти на Structured Output; не парсить как JS |
| Сломанная строка | Неэкранированная ", сырые переносы, полноширинные запятые | Строка закрывается рано или нет : после ключа | Смотреть колонку; ограничить длину поля |
| Обрыв | Незакрытый объект или массив | Unexpected end of JSON input | Поднять потолок вывода; дождаться конца стрима |
| Несколько значений | Два JSON-значения или проза после первого | Символы после первого значения | Вырезать только первое полное значение |
| Кодировка | BOM, zero-width, двойной stringify | Странный токен или parse даёт строку | Снять BOM; проверить typeof перед вторым parse |
Для агентов ещё одно: arguments у Tool Calling часто уже объект или JSON-строка, которую вендор уже ограничил. Не гоняйте всё сообщение ассистента через JSON.parse. Это другой канал — см. почему Tool Calling зависит от JSON Schema.
Ограждения и обёрточная проза
Чат-модели обучены класть код в ограждения. Даже если вы написали «только JSON», ответ часто такой:
```json
{"ok": true, "id": "A-1024"}
```
Here is the result. I can explain the fields if you want.
Первый символ — обратная кавычка, не {. JSON.parse падает на колонке 0. Вводное «Конечно, вот JSON:» или дисклеймер в конце — тот же баг. Хуже: два значения — сначала образец, потом настоящий результат. Парсите весь блоб — умрёте после первой }.
Правило извлечения одно: найдите первую сбалансированную {} или [] (скобки внутри строк пропускайте) и отдайте JSON.parse только этот срез. Ограждения снимайте сначала. Не режьте жадно от первой { до последней } — скобки в строках или второй объект в объяснении срежут неправильно.
Диалект: висячие запятые, одинарные кавычки, комментарии, голые ключи
Модели видели массы JavaScript, Python, JSON5 и YAML. Когда просят «структурированные данные», они смешивают диалекты. Для JSON.parse всё ниже незаконно:
{
ok: true, // bare key + comment
'name': 'Ada', // single quotes
"tags": ["a",], // trailing comma
"flag": True // Python boolean
}
Плюс undefined, NaN, Infinity, None. В своих языках они что-то значат; в JSON есть null и конечные числа. Менять JSON.parse на eval или new Function, чтобы «принять» эти формы, — превратить парсер в сток произвольного кода. В продакшене так не делают.
JSON5 и JSONC проглатывают комментарии и висячие запятые. Для людей, которые правят конфиг, это нормально. Как парсер модели по умолчанию — плохо. Ослабив грамматику, вы больше не отличите «лишняя запятая» от «сломанная строка». Если нужен свободный слой — держите его за отказом extract + parse и после починки всё равно гоните Schema.
Строки и пунктуация: экранирование, переносы, полноширинные и «умные» кавычки
Легальная JSON-строка — в двойных кавычках. Внутренние " и обратные слэши экранируют. Управляющие символы — \n, \t или \uXXXX. Когда модель копирует комментарий пользователя, сырые кавычки и переносы падают внутрь поля. Строка закрывается рано; следующая запятая или CJK-символ становится unexpected token.
CJK-вывод добавляет частый набор грязи: полноширинная запятая ,, полноширинное двоеточие :, фигурные кавычки “” / ‘’. Выглядят как пунктуация; кодовые точки — не 0x2C / 0x3A / 0x22. Этот «почти JSON» умирает после значения name:
{
"name": "Ada",
"ok": true
}
Не лечите это ещё одной фразой «пожалуйста, ASCII-пунктуация». Поставьте maxLength на длинные строковые поля, пусть модель цитирует исходный текст, а не перепечатывает знаки, и на финальном канале включите Structured Output. Для отладки вставьте в валидатор JSON и смотрите, на какой колонке остановилась подсветка — полноширинная запятая видна сразу.
Обрыв и стриминг: Unexpected end of JSON input
Unexpected end of JSON input почти всегда значит текст кончился раньше грамматики: нет }, нет ] или строка не закрыта. В 2026 обычные источники: потолок выходных токенов, обрыв фильтром безопасности или вы вызвали JSON.parse на неполном чанке стрима.
Стриминговый API отдаёт дельты. Ранние чанки могут быть {"ok": tr. Парсите — упадёте. Делайте так:
- Дождитесь конца стрима (
finish_reason/stop), затем парсите полный буфер; - Или настоящий потоковый JSON-парсер, который идёт токен за токеном — не вызывайте
JSON.parseна половине значения; - Если причина остановки —
length/max_tokens, это не баг парсинга. Генерация не дописала — поднимите потолок, сузьте Schema или разбейте модель на страницы.
Автозакрытие скобок после обрыва — трюк для черновика. Форма может распарситься и всё равно потерять поля или разрезать строку. После любой починки — Schema-валидация; при отказе — ретрай. Молча в базу не кладите.
Невидимые символы и двойное кодирование
UTF-8 BOM (U+FEFF) — не JSON-пробел. Некоторые пути копирования и шлюзы ставят его в префикс; JSON.parse тогда сообщает unexpected token на колонке 0. Zero-width spaces и мягкие переносы — то же. Снимайте replace(/^\uFEFF/, ""), затем trim, до извлечения.
Двойное кодирование тише. Один JSON.stringify даёт строку "{\"ok\":true}". Распарсите эту форму в кавычках — получите строку {"ok":true}, не объект. Второй parse даёт объект. Если остановитесь после одного parse и прочитаете .ok, получите undefined — «распарсилось», полей нет. Проверяйте typeof перед вторым parse. Не хардкодьте «всегда парсить дважды»: настоящий объект бросит исключение.
Порядок фикса: сменить канал, потом извлечь, чинить в конце
Этот порядок бьёт стопку лишних фраз в промпте:
- Смените канал. Финальные ответы — через Structured Output (OpenAI
response_format.json_schema, GeminiresponseMimeTypeплюс Schema, Claudeoutput_config.format). Параметры инструментов — черезargumentsTool Calling, не выскребая из прозы. См. что такое Structured Output. - Извлеките. Снимите ограждения
```json; вырежьте первое сбалансированное значение; уберите BOM. - Парсите строго. Только
JSON.parse. При отказе сохраните сырой текст и позицию ошибки. Неeval. - Проверьте Schema. Успешный parse значит только легальную грамматику. Нет полей, не те типы, лишние ключи — это JSON Schema / ajv. См. как ИИ генерирует JSON по JSON Schema.
- Чините последним. Инструменты вроде
jsonrepairзакрывают скобки и снимают висячие запятые. Только после отказа extract + parse и только если вы принимаете, что починка может сменить смысл. Затем всё равно шаги 3 и 4. Не делайте ремонтник глобальным парсером по умолчанию.
Промпты всё ещё помогают: «без ограждений, без объяснений». Они снижают шанс обёртки. Они не заменяют Schema и не ослабляют JSON.parse. В 2026 считать чат-прозу API — значит снова и снова платить за ограждения и обрывы.
Короткий пайплайн: извлечь + parse
Учебный пайплайн: снять ограждения, убрать BOM, вырезать сбалансированное значение, затем JSON.parse. Ловит обычные обёртки. Не чинит висячие запятые и полноширинную пунктуацию — это Structured Output или явный ремонтный слой.
function stripFence(text) {
const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
return m ? m[1] : String(text);
}
function sliceBalancedJson(text) {
const src = text.replace(/^\uFEFF/, "").trim();
const start = src.search(/[\{\[]/);
if (start < 0) throw new SyntaxError("No JSON value found");
const open = src[start];
const close = open === "{" ? "}" : "]";
let depth = 0, inStr = false, esc = false;
for (let i = start; i < src.length; i++) {
const ch = src[i];
if (inStr) {
if (esc) { esc = false; continue; }
if (ch === "\\") { esc = true; continue; }
if (ch === '"') inStr = false;
continue;
}
if (ch === '"') { inStr = true; continue; }
if (ch === open) depth++;
else if (ch === close) {
depth--;
if (depth === 0) return src.slice(start, i + 1);
}
}
throw new SyntaxError("Unterminated JSON value");
}
function parseModelJson(raw) {
return JSON.parse(sliceBalancedJson(stripFence(raw)));
}
Код среза обязан помнить, внутри ли он строки — иначе { в значении поля закроется слишком рано. Вложенные объекты и массивы — через depth. Если срез всё равно не парсится, вставьте упавший текст в валидатор и смотрите таблицу классов выше. Не наваливайте на этот слой ещё регулярки.
Смотреть ошибку локально
Не отправляйте вывод модели сразу в продовый парсер. В браузере проверьте три вещи: это легальный JSON; если нет — на какой колонке; если Schema уже есть — проходит ли контракт.
- Валидатор JSON — где садится
SyntaxError; приложите Schema, если она есть. - Форматирование JSON — если форматируется, обычно парсится; если нет — ищите полноширинные запятые или ограждения в исходнике.
- JSON Diff — после успешного parse сравните объект модели с минимальным объектом, который вы разрешаете.
Ничего не уходит из браузера. Так удобно положить рядом упавший ответ модели, Schema и blob arguments Tool Calling. Стабилизируйте имена полей и required, потом подключайте Host.
FAQ
Почему «похоже на JSON» всё равно не проходит JSON.parse?
Человеческий глаз терпит ограждения, висячие запятые, фигурные кавычки и обёрточную прозу. JSON.parse принимает ровно одно значение по RFC 8259. Быть похожим на JSON — не значит быть легальным JSON.
Хватит ли регулярки, которая снимает ```json ограждения?
Нет. Ограждения — лишь одна обёртка. Дальше остаются хвостовая проза, второе JSON-значение, висячие запятые и обрыв. После снятия ограждений вырежьте сбалансированное значение и парсите строго.
Чем JSON Mode отличается от Structured Output?
JSON Mode обычно только ограничивает «похоже на JSON», не поля и типы. Structured Output режет незаконные токены по JSON Schema на этапе декодирования. Если результат съест программа — берите Structured Output. Не включайте JSON Mode и затем JSON.parse чат-тело.
Делать jsonrepair или JSON5 парсером по умолчанию?
Нет. Они принимают ввод, который должен упасть, и могут сменить смысл. Только как ремонтный слой после отказа extract + JSON.parse, затем всё равно Schema-валидация.
Когда можно вызывать JSON.parse на стриме?
Когда стрим закончился и буфер — полное значение. Парсинг половины чанка каждый раз даёт Unexpected end of JSON input. Чтобы есть токены по мере прихода — потоковый парсер, не JSON.parse.
Parse прошёл, поля не те. Это эта статья?
Это следующий слой. JSON.parse гарантирует только грамматику. Нет полей, не те типы, лишние ключи — проблемы Schema; см. материалы сайта про Structured Output и валидацию Tool Calling.
Итог
Отказ JSON.parse — проблема канала. Ещё одна фраза «пожалуйста, верни JSON» её не лечит. Чат-модели ставят ограждения, смешивают диалекты и останавливаются на потолке токенов. Парсер принимает одно чистое JSON-значение. В 2026 подключайте модель через Structured Output или arguments Tool Calling; затем извлечь + строгий parse + Schema; чинить — в конце.
Промпты снижают обёртки. Они не ослабляют грамматику. Вставьте упавший текст в локальный валидатор, смотрите колонку остановки, затем решайте: снять ограждение, сменить канал или поднять потолок вывода. Модели меняются. Что принимает JSON.parse и каков контракт ваших полей — нет.