Почему ПрочитатьJSON спотыкается на первом же символе и как это починить

· 9 мин

Содержание

Ошибка «непредвиденный символ при чтении JSON» в 1С почти всегда значит одно: в ПрочитатьJSON пришёл не JSON или JSON с мусором в начале. Чаще всего это невидимый маркер BOM (символ с кодом 65279), HTML-страница ошибки вместо ответа API или текст в неверной кодировке. Исправление — посмотреть первые символы строки, убрать лишнее и читать данные в правильной кодировке.

Платформа не «ломается» и не теряет данные: парсер ЧтениеJSON строгий и останавливается на первом символе, который не вписывается в грамматику JSON. Формулировка сообщения немного отличается в разных версиях платформы, обычно в нём есть номер строки и позиция. Позиция 1 — проблема в самом начале текста, большая позиция — испорчено содержимое.

Откуда берётся непредвиденный символ в JSON

Парсер ждёт в начале { или [ (реже строку или число), а внутри — только правильно экранированные значения. Всё остальное он считает ошибкой синтаксического анализа. Источники мусора на практике такие:

  • 🔹 BOM в начале текста. Файл или ответ сохранён в UTF-8 с маркером порядка байт EF BB BF. После декодирования в строку это невидимый символ с кодом 65279, и разбор падает на позиции 1.
  • 🌐 HTML вместо JSON. Сервер вернул страницу 404, 502, форму авторизации или заглушку прокси. Первый символ — <.
  • 📭 Пустой ответ. Код 204, обрыв соединения, таймаут. Строка пустая или из одних пробелов.
  • 🔤 UTF-16. Тело пришло в двухбайтовой кодировке, а читаете вы как UTF-8. Между буквами оказываются нулевые байты.
  • 📦 Сжатое тело. В ответе Content-Encoding: gzip, а до программы дошли сырые байты. Первые байты 1F 8B.
  • 🧩 JSONP и обёртки. Ответ вида callback({...}) или )]}' в первой строке — так некоторые сервисы защищаются от встраивания.
  • ✂️ «Почти JSON». Одинарные кавычки, запятая после последнего элемента, комментарии //, значения NaN и undefined. Браузер такое иногда прощает, 1С — нет.
  • ⛔ Неэкранированные управляющие символы. Табуляция или перевод строки внутри строкового значения без \t и \n. Типично для JSON, склеенного вручную конкатенацией строк.
  • 📚 Несколько объектов подряд. Формат NDJSON (один объект на строку) или два ответа, склеенные в один файл. Первый объект читается, на втором — ошибка.

Расхождение Windows-1251 и UTF-8 обычно даёт не эту ошибку, а «кракозябры» в русских значениях. Служебные символы JSON — латиница, и они одинаковы в обеих кодировках. Если ошибка есть, а русский текст читается нормально, ищите BOM, обёртку или управляющие символы.

Как найти символ, на котором падает разбор

Не гадайте по тексту ошибки: выведите, что на самом деле пришло. Понадобятся три вещи: код ответа, заголовок типа содержимого и коды первых символов.

Ответ = Соединение.Получить(Запрос);
Тело = Ответ.ПолучитьТелоКакСтроку("UTF-8");

Сообщить("Код ответа: " + Формат(Ответ.КодСостояния, "ЧГ=0"));
Сообщить("Content-Type: " + Строка(Ответ.Заголовки.Получить("Content-Type")));
Сообщить("Длина: " + Формат(СтрДлина(Тело), "ЧН=0; ЧГ=0"));

Для Н = 1 По Мин(10, СтрДлина(Тело)) Цикл
    Сообщить(Формат(Н, "ЧГ=0") + ": «" + Сред(Тело, Н, 1) + "» код "
        + Формат(КодСимвола(Тело, Н), "ЧГ=0"));
КонецЦикла;

Ключи в Заголовки чувствительны к регистру. Если Content-Type вернулся пустым, попробуйте content-type — часть серверов пишет заголовки строчными буквами.

Для разбора на уровне байтов сохраните тело как есть, без декодирования:

ДД = Ответ.ПолучитьТелоКакДвоичныеДанные();
ДД.Записать(КаталогВременныхФайлов() + "answer.json");
Сообщить(Лев(ПолучитьHexСтрокуИзДвоичныхДанных(ДД), 40));

Файл откройте в редакторе, который показывает кодировку и спецсимволы. Шестнадцатеричная строка сразу покажет BOM (EFBBBF), UTF-16 (FFFE или FEFF и нули через байт) или gzip (1F8B).

Диагностика: выберите симптом

Если позиция ошибки большая, вырежьте фрагмент вокруг неё: Сред(Тело, Позиция - 50, 100). Учтите, что в многострочном JSON позиция может отсчитываться от начала строки, а не от начала текста — тогда сначала найдите нужную строку по номеру через СтрПолучитьСтроку.

Очистка текста перед парсингом JSON

Чистить имеет смысл только то, что нельзя исправить у источника. Функция ниже снимает типичные «наслоения» и не трогает данные внутри значений:

Функция ПодготовитьТекстJSON(Знач Текст) Экспорт

    // BOM после декодирования — символ с кодом 65279
    Если СтрДлина(Текст) > 0 И КодСимвола(Текст, 1) = 65279 Тогда
        Текст = Сред(Текст, 2);
    КонецЕсли;

    Текст = СокрЛП(Текст);

    // Обёртка JSONP: callback({...});
    Первый = Лев(Текст, 1);
    Если Первый <> "{" И Первый <> "[" Тогда
        Начало = СтрНайти(Текст, "(");
        Конец = СтрНайти(Текст, ")", НаправлениеПоиска.СКонца);
        Если Начало > 0 И Конец > Начало Тогда
            Текст = СокрЛП(Сред(Текст, Начало + 1, Конец - Начало - 1));
        КонецЕсли;
    КонецЕсли;

    // Управляющие символы, которых в корректном JSON быть не может
    Для Код = 1 По 31 Цикл
        Если Код = 9 Или Код = 10 Или Код = 13 Тогда
            Продолжить;
        КонецЕсли;
        Текст = СтрЗаменить(Текст, Символ(Код), "");
    КонецЦикла;

    Первый = Лев(Текст, 1);
    Если Первый <> "{" И Первый <> "[" Тогда
        ВызватьИсключение "Ответ не похож на JSON: " + Лев(Текст, 200);
    КонецЕсли;

    Возврат Текст;

КонецФункции

Что функция делает намеренно не так, как хочется:

  1. Не удаляет табуляцию и переводы строк. Между элементами они законны. Внутри строкового значения — нет, но отличить одно от другого без полного разбора нельзя. Такой JSON надо чинить у отправителя.
  2. Не превращает одинарные кавычки в двойные. Замена сломает значения, в которых апостроф — часть текста.
  3. Падает с понятным сообщением, если после очистки текст не начинается с { или [. Это лучше, чем «непредвиденный символ» без контекста.

Не вырезайте всё до первой { «на всякий случай». Если пришла HTML-страница с примером JSON внутри, вы молча разберёте не те данные.

NDJSON (по объекту на строку) не очищают, а читают построчно: каждую непустую строку отдельно через УстановитьСтроку и ПрочитатьJSON, результаты складывают в массив.

Кодировка и потоки: как читать JSON без сюрпризов

Самая частая причина ошибки при загрузке из файла и HTTP — двойное декодирование: сначала байты превращают в строку с неверной кодировкой, потом строку отдают парсеру. Надёжнее отдавать ЧтениеJSON файл или поток и явно указывать кодировку.

ИсточникКак читатьКодировка
Файл на дискеЧтение.ОткрытьФайл(ИмяФайла, "UTF-8")вторым параметром
Двоичные данныеЧтение.ОткрытьПоток(ДД.ОткрытьПотокДляЧтения(), "UTF-8")вторым параметром
Тело HTTP-ответаОтвет.ПолучитьТелоКакСтроку("UTF-8") → УстановитьСтрокупри получении строки
Готовая строкаЧтение.УстановитьСтроку(Текст)уже декодирована

Пример для файла в Windows-1251, который выгрузила старая программа:

Чтение = Новый ЧтениеJSON;
Чтение.ОткрытьФайл(ИмяФайла, "windows-1251");
Данные = ПрочитатьJSON(Чтение, Истина);
Чтение.Закрыть();

Если кодировку источника вы не знаете, получите двоичные данные и посмотрите первые байты, как в разделе про диагностику. ПолучитьСтрокуИзДвоичныхДанных(ДД, КодировкаТекста.UTF8) даёт строку с явной кодировкой, дальше — функция очистки.

Второй параметр Истина в ПрочитатьJSON возвращает Соответствие вместо Структуры. Это не про символы, но спасает от соседней ошибки: ключ JSON с пробелом, дефисом или цифрой в начале нельзя сделать именем свойства структуры.

Не путайте ПрочитатьJSON и СериализаторXDTO.ПрочитатьJSON. Второй ждёт JSON в формате сериализации самой 1С — со служебными полями #type и #value. Произвольный ответ стороннего API через XDTO не прочитается.

Попытка-Исключение и журнал регистрации при ошибках чтения JSON

Ошибка разбора в регламентном задании или обмене без обработки обрывает весь процесс. Оборачивайте чтение в Попытка и пишите в журнал регистрации не только текст ошибки, но и начало ответа — без него через неделю причину не восстановить.

Чтение = Новый ЧтениеJSON;
Попытка
    Чтение.УстановитьСтроку(ПодготовитьТекстJSON(Тело));
    Данные = ПрочитатьJSON(Чтение, Истина);
    Чтение.Закрыть();
Исключение
    Инфо = ИнформацияОбОшибке();
    ЗаписьЖурналаРегистрации("Обмен.ЗагрузкаJSON",
        УровеньЖурналаРегистрации.Ошибка, , ,
        ПодробноеПредставлениеОшибки(Инфо) + Символы.ПС
        + "Код ответа: " + Формат(Ответ.КодСостояния, "ЧГ=0") + Символы.ПС
        + "Начало ответа: " + Лев(Тело, 500));
    Данные = Неопределено;
КонецПопытки;

Чего в логе быть не должно:

  • 🔑 токенов, паролей и заголовков авторизации — журнал читают многие;
  • 📄 полного тела мегабайтного ответа — 500 символов хватает для диагноза, остальное раздувает журнал.

Найти записи потом просто: Администрирование → Обслуживание → Журнал регистрации, отбор по событию Обмен.ЗагрузкаJSON и уровню «Ошибка». Имя события придумайте своё, но одно на весь обмен.

Внешние API и HTTP-сервисы: ошибки JSON на стыке систем

1С получает данные

  1. Проверяйте код ответа до разбора. Разбирайте тело только при кодах 200–299. При 401, 403, 500 и прочих пишите в лог код и начало тела — там почти всегда HTML или текстовое сообщение сервера.
  2. Смотрите Content-Type. Если там text/html, разбирать нечего. Если application/json; charset=... — берите кодировку оттуда.
  3. Учитывайте прокси и балансировщики. Страница «502 Bad Gateway» от nginx приходит в HTML, даже если сам сервис всегда отвечает JSON.
  4. Задайте таймаут. Параметр Таймаут у HTTPСоединение спасает от обрезанного ответа на медленном канале. Обрезанный JSON падает с ошибкой где-то в конце текста.

1С принимает запрос в своём HTTP-сервисе

Если ошибка возникает в модуле HTTP-сервиса при разборе входящего запроса, проблема обычно в клиенте, но защищаться нужно на своей стороне:

  • 📨 Клиент шлёт форму, а не JSON. Тело вида name=value&id=5 приходит с типом application/x-www-form-urlencoded. Проверяйте заголовок Content-Type входящего запроса и отвечайте кодом 415 с понятным текстом.
  • 🔤 Клиент не указал кодировку. Читайте тело с явной кодировкой: Запрос.ПолучитьТелоКакСтроку("UTF-8"), а не без параметра.
  • 🧪 Тестовый запрос из Postman или curl. Частая ловушка — одинарные кавычки вокруг тела в командной строке Windows, которые уходят внутрь JSON. Сначала повторите запрос из файла с телом.

При ошибке разбора возвращайте клиенту код 400 и короткое сообщение с позицией. Так отправитель найдёт свой дефект сам, а не будет писать вам в поддержку.

1С отправляет данные или отвечает как HTTP-сервис

Здесь ошибку ловит другая сторона, но причина у вас. Чаще всего это BOM в теле, который добавляет сама 1С, и JSON, собранный конкатенацией строк.

Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, ДанныеОтправки);
ТекстJSON = Запись.Закрыть();

Запрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Запрос.УстановитьТелоИзСтроки(ТекстJSON, КодировкаТекста.UTF8,
    ИспользованиеByteOrderMark.НеИспользовать);

Тот же третий параметр есть у УстановитьТелоИзСтроки для ответа HTTP-сервиса. Если внешняя система жалуется на «unexpected token» в позиции 0 — это почти наверняка BOM, и этот параметр его убирает.

Не собирайте JSON сложением строк. ЗаписатьJSON сам экранирует кавычки, обратные слэши и переводы строк. Ручная склейка ломается на первом же наименовании вроде ООО "Ромашка" или комментарии из двух строк.

Частые вопросы

Почему тот же файл открывается в браузере, а 1С пишет ошибку?
Браузер и многие редакторы молча пропускают BOM и прощают мелкие нарушения формата. Парсер 1С строгий: проверьте первый символ и наличие комментариев или висящих запятых.
Ошибка появилась после обновления платформы, хотя код не меняли. Что проверить?
Сначала сам ответ сервиса: сменился адрес, протокол или сервис стал отдавать заглушку. Если ответ прежний — сравните байты тела до и после, обычно меняется кодировка или появляется BOM.
Можно ли прочитать JSON с комментариями, например файл настроек?
Встроенный парсер комментарии не принимает. Удалите их перед разбором или храните настройки без комментариев — вынесите пояснения в отдельные поля.
Ошибка возникает только на некоторых записях из большой выгрузки. Как найти виновника?
Разбейте выгрузку на части и читайте по одной, записывая номер части в лог. Обычно виновник — значение с табуляцией, переводом строки или управляющим символом, вставленным из буфера обмена.
Что делать, если ответ огромный и строка не помещается в память?
Не превращайте его в строку: откройте поток двоичных данных через ОткрытьПоток и читайте последовательно методом Прочитать объекта ЧтениеJSON, обрабатывая элементы по одному.