Как правильно сформировать JSON-массив из нескольких объектов в 1С?

Программист 1С v8.3 (Управляемые формы)
← На главную

При работе с данными в 1С часто возникает необходимость экспортировать их в формат JSON для обмена с внешними системами или хранения. Однако при попытке записать последовательность объектов в JSON-файл мы можем столкнуться с проблемой, когда результирующий файл оказывается невалидным, содержащим синтаксические ошибки, такие как лишние запятые или некорректная структура. В этой статье мы подробно разберем, почему возникает такая ситуация и как правильно формировать JSON-массивы из множества объектов в 1С, используя наиболее эффективные подходы.

Анализ проблемы и некорректный подход

Начнем с рассмотрения типичного кода, который приводит к некорректной записи JSON-файла, как это было описано в сообщении пользователя:


Функция Создатьjson()
    ЗаписьДж = Новый ЗаписьJSON;
    ЗаписьДж.ПроверятьСтруктуру = Ложь; // Здесь может скрываться корень проблемы
    ПараметрыЗаписиДж = Новый ПараметрыЗаписиJSON( , Символы.Таб);
    ЗаписьДж.ОткрытьФайл("C:\ФайлВыгрузки.json",,,ПараметрыЗаписиДж);

    // ... Запрос к базе данных и выборка данных ...

    Пока ВыборкаДетальныеЗаписи.Следующий Цикл
        Структура1 = новый Структура;
        Структура1.Вставить("productName" , Строка(ВыборкаДетальныеЗаписи.Наименование));
        // ... Заполнение других свойств структуры ...

        ЗаписатьJSON(ЗаписьДж, Структура1); // Повторный вызов ЗаписатьJSON внутри цикла
    КонецЦикла;

    ЗаписьДж.Закрыть;
КонецФункции

Результатом выполнения такого кода является файл (для анализа которого удобно использовать просмотр JSON с форматированием), который выглядит примерно так:

{ "productName": "Оборудование (объекты основных средств)", "code": "00-00000004" }, {, "productName": , "Оборудование к установке", "code": , "00-00000005" },

или даже так:

{ "productName": ..., "code": ... }{ "productName": ..., "code": ... }

Это не является валидным JSON-файлом. Согласно стандарту JSON, корневой элемент документа должен быть либо одним объектом (заключенным в фигурные скобки {}), либо одним массивом (заключенным в квадратные скобки []). В нашем случае, многократный вызов метода ЗаписатьJSON(ЗаписьДж, Структура1) внутри цикла приводит к тому, что в файл записываются несколько независимых JSON-объектов, конкатенированных друг за другом. Это нарушает синтаксис JSON.

Влияние свойства ЗаписьJSON.ПроверятьСтруктуру

Обратите внимание на строку ЗаписьДж.ПроверятьСтруктуру = Ложь; в исходном коде. Это свойство объекта ЗаписьJSON по умолчанию имеет значение Истина. Когда оно установлено в Истина, платформа 1С автоматически проверяет валидность формируемой JSON-структуры. Если структура нарушена (например, вы пытаетесь записать несколько корневых объектов), генерируется исключение, и процесс записи останавливается.

Установка ЗаписьДж.ПроверятьСтруктуру = Ложь; отключает эту проверку. Это позволяет системе "проглотить" синтаксическую ошибку и записать невалидный JSON без генерации исключения, что, казалось бы, "решает" проблему, но на самом деле лишь скрывает ее. В результате мы получаем битый файл. Рекомендуем всегда держать это свойство в Истина во время разработки и отладки, чтобы своевременно выявлять ошибки структуры. Отключать его следует только в случаях, когда вы абсолютно уверены в корректности формируемого JSON и вам нужна небольшая оптимизация производительности при работе с очень большими объемами данных.

Правильные способы формирования JSON-массива

Рассмотрим два основных способа, которые позволяют корректно сформировать JSON-массив из коллекции объектов в 1С.

1. Использование объекта Массив для сбора данных

Наиболее простой и часто рекомендуемый способ — это сначала собрать все структуры, которые должны стать элементами JSON-массива, в один объект Массив 1С, а затем вызвать метод ЗаписатьJSON() один раз, передав ему этот собранный массив. Объект ЗаписьJSON самостоятельно сериализует Массив 1С в корректный JSON-массив.

Разберем этот подход по шагам:

  1. Создаем новый объект Массив: Этот массив будет хранить все наши структуры, которые в конечном итоге станут элементами JSON-массива.
  2. Наполняем Массив структурами: В цикле мы создаем каждую структуру (представляющую один JSON-объект) и добавляем ее в созданный Массив.
  3. Вызываем ЗаписатьJSON() один раз: После того как все структуры собраны в Массив, мы вызываем метод ЗаписатьJSON(), передавая ему этот Массив.

Пример кода:


Функция СоздатьjsonКорректноЧерезМассив()
    ЗаписьДж = Новый ЗаписьJSON;
    ЗаписьДж.ПроверятьСтруктуру = Истина; // Рекомендуется для отладки
    ПараметрыЗаписиДж = Новый ПараметрыЗаписиJSON( , Символы.Таб); // Параметры форматирования для читаемости
    ЗаписьДж.ОткрытьФайл("C:\ФайлВыгрузки.json",,,ПараметрыЗаписиДж);

    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ 
                   | Номенклатура.Наименование КАК Наименование, 
                   | Номенклатура.Артикул КАК Артикул, 
                   | Номенклатура.ЕдиницаИзмерения.Наименование КАК ЕдиницаИзмеренияНаименование, 
                   | Номенклатура.ВидНоменклатуры.Наименование КАК ВидНоменклатурыНаименование, 
                   | Номенклатура.Ссылка КАК Ссылка, 
                   | Номенклатура.СтавкаНДС.Наименование КАК СтавкаНДСНаименование 
                   |ИЗ 
                   | Справочник.Номенклатура КАК Номенклатура";
    РезультатЗапроса = Запрос.Выполнить();
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

    ДанныеДляJSON = Новый Массив; // Создаем объект Массив для сбора всех структур

    Пока ВыборкаДетальныеЗаписи.Следующий Цикл
        Структура1 = новый Структура;
        Структура1.Вставить("productName" , Строка(ВыборкаДетальныеЗаписи.Наименование));

        Если Не ВыборкаДетальныеЗаписи.Артикул = Null Тогда
            Структура1.Вставить("articul" , Строка(ВыборкаДетальныеЗаписи.Артикул));
        Иначе
            Структура1.Вставить("articul", ""); // Важно: всегда указывать значение, даже пустое, чтобы избежать ", "key": ,"
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.ЕдиницаИзмеренияНаименование = Null Тогда
            Структура1.Вставить("package" , Строка(ВыборкаДетальныеЗаписи.ЕдиницаИзмеренияНаименование));
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.ВидНоменклатурыНаименование = Null Тогда
            Структура1.Вставить("vid" , Строка(ВыборкаДетальныеЗаписи.ВидНоменклатурыНаименование));
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.СтавкаНДСНаименование = Null Тогда
            Структура1.Вставить("nds" , Строка(ВыборкаДетальныеЗаписи.СтавкаНДСНаименование));
        КонецЕсли;

        // Дополнительные запросы для сбора данных по остаткам и ценам
        Запрос1 = Новый Запрос;
        Запрос1.Текст = "ВЫБРАТЬ 
                       | ТоварыНаСкладахОстатки.Номенклатура.Ссылка КАК НоменклатураСсылка, 
                       | ТоварыНаСкладахОстатки.Склад.Наименование КАК СкладНаименование, 
                       | ТоварыНаСкладахОстатки.ВНаличииОстаток КАК ВНаличииОстаток 
                       |ИЗ 
                       | РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки 
                       |ГДЕ 
                       | ТоварыНаСкладахОстатки.Номенклатура.Ссылка = &Ссылка";
        Запрос1.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
        РезультатЗапроса1 = Запрос1.Выполнить();
        ВыборкаДетальныеЗаписи1 = РезультатЗапроса1.Выбрать();

        // Важно: если ожидается несколько остатков по разным складам,
        // то Ostatok и sklad должны быть массивом объектов внутри Структура1.
        // Для упрощения примера, возьмем только первый найденный или один объект.
        // Если же должно быть несколько остатков, нужно формировать массив.
        // Например:
        // МассивОстатков = Новый Массив;
        // Пока ВыборкаДетальныеЗаписи1.Следующий Цикл
        //    СтруктураОстатка = Новый Структура;
        //    СтруктураОстатка.Вставить("Количество", Строка(ВыборкаДетальныеЗаписи1.ВНаличииОстаток));
        //    СтруктураОстатка.Вставить("Склад", Строка(ВыборкаДетальныеЗаписи1.СкладНаименование));
        //    МассивОстатков.Добавить(СтруктураОстатка);
        // КонецЦикла;
        // Если МассивОстатков.Количество() > 0 Тогда
        //    Структура1.Вставить("stocks", МассивОстатков);
        // КонецЕсли;

        Если ВыборкаДетальныеЗаписи1.Следующий() Тогда // Только первый остаток для примера
            Если Не ВыборкаДетальныеЗаписи1.ВНаличииОстаток = Null Тогда
                Структура1.Вставить("Ostatok" , Строка(ВыборкаДетальныеЗаписи1.ВНаличииОстаток));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи1.СкладНаименование = Null Тогда
                Структура1.Вставить("sklad" , Строка(ВыборкаДетальныеЗаписи1.СкладНаименование));
            КонецЕсли;
        КонецЕсли;

        Запрос2 = Новый Запрос;
        Запрос2.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 
                       | ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура, 
                       | ЦеныНоменклатурыСрезПоследних.ВидЦены.Наименование КАК ВидЦеныНаименование, 
                       | ЦеныНоменклатурыСрезПоследних.Цена КАК Цена, 
                       | ЦеныНоменклатурыСрезПоследних.Валюта.Наименование КАК ВалютаНаименование 
                       |ИЗ 
                       | РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних 
                       |ГДЕ 
                       | ЦеныНоменклатурыСрезПоследних.Номенклатура.Ссылка = &Ссылка 
                       | 
                       |УПОРЯДОЧИТЬ ПО 
                       | ЦеныНоменклатурыСрезПоследних.Период УБЫВ";
        Запрос2.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
        РезультатЗапроса2 = Запрос2.Выполнить();
        ВыборкаДетальныеЗаписи2 = РезультатЗапроса2.Выбрать();

        Если ВыборкаДетальныеЗаписи2.Следующий() Тогда // Только первая цена для примера
            Если Не ВыборкаДетальныеЗаписи2.Цена = Null Тогда
                Структура1.Вставить("price" , Строка(ВыборкаДетальныеЗаписи2.Цена));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи2.ВидЦеныНаименование = Null Тогда
                Структура1.Вставить("vidPrice" , Строка(ВыборкаДетальныеЗаписи2.ВидЦеныНаименование));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи2.ВалютаНаименование = Null Тогда
                Структура1.Вставить("Valuta" , Строка(ВыборкаДетальныеЗаписи2.ВалютаНаименование));
            КонецЕсли;
        КонецЕсли;
        
        ДанныеДляJSON.Добавить(Структура1); // Добавляем каждую структуру в Массив
    КонецЦикла;

    ЗаписатьJSON(ЗаписьДж, ДанныеДляJSON); // Однократный вызов ЗаписатьJSON для всего Массива

    ЗаписьДж.Закрыть;
КонецФункции

Этот подход гарантирует, что на выходе мы получим корректный JSON-массив, который будет выглядеть так:

[ { "productName": "Шкаф мед. ...", "articul": "", ... }, { "productName": "Банкетка ...", "articul": "", ... } ]

Обратите внимание на то, что мы добавили проверку на Null значения и в случае Null для некоторых полей (например, артикула) вставляем пустую строку "". Это важно для предотвращения формирования записей типа ", "articul": ,", где после двоеточия нет значения, что также является синтаксической ошибкой JSON.

2. Потоковая запись с явным указанием границ массива

Второй способ используется, когда объем данных настолько велик, что их нецелесообразно полностью загружать в память в виде объекта Массив. В таких случаях мы используем методы потоковой записи объекта ЗаписьJSON, явно указывая начало и конец массива.

Разберем этот подход по шагам:

  1. Открываем JSON-файл и начинаем массив: Сначала мы вызываем ЗаписьДж.ЗаписатьНачалоМассива(), который записывает открывающую квадратную скобку [.
  2. В цикле записываем каждый объект: Внутри цикла мы используем методы ЗаписьДж.ЗаписатьНачалоОбъекта(), ЗаписьДж.ЗаписатьИмяСвойства(), ЗаписьДж.ЗаписатьЗначение() для формирования каждого отдельного JSON-объекта. Завершаем объект вызовом ЗаписьДж.ЗаписатьКонецОбъекта().
  3. Завершаем массив: После выхода из цикла мы вызываем ЗаписьДж.ЗаписатьКонецМассива(), который записывает закрывающую квадратную скобку ].

Пример кода:


Функция СоздатьjsonПотоково()
    ЗаписьДж = Новый ЗаписьJSON;
    ЗаписьДж.ПроверятьСтруктуру = Истина; // Рекомендуется
    ПараметрыЗаписиДж = Новый ПараметрыЗаписиJSON( , Символы.Таб); // Параметры форматирования
    ЗаписьДж.ОткрытьФайл("C:\ФайлВыгрузкиПотоково.json",,,ПараметрыЗаписиДж);

    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ 
                   | Номенклатура.Наименование КАК Наименование, 
                   | Номенклатура.Артикул КАК Артикул, 
                   | Номенклатура.ЕдиницаИзмерения.Наименование КАК ЕдиницаИзмеренияНаименование, 
                   | Номенклатура.ВидНоменклатуры.Наименование КАК ВидНоменклатурыНаименование, 
                   | Номенклатура.Ссылка КАК Ссылка, 
                   | Номенклатура.СтавкаНДС.Наименование КАК СтавкаНДСНаименование 
                   |ИЗ 
                   | Справочник.Номенклатура КАК Номенклатура";
    РезультатЗапроса = Запрос.Выполнить();
    ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();

    ЗаписьДж.ЗаписатьНачалоМассива(); // Начинаем JSON-массив

    Пока ВыборкаДетальныеЗаписи.Следующий Цикл
        ЗаписьДж.ЗаписатьНачалоОбъекта(); // Начинаем очередной JSON-объект

        ЗаписьДж.ЗаписатьИмяСвойства("productName");
        ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи.Наименование));

        ЗаписьДж.ЗаписатьИмяСвойства("articul");
        Если Не ВыборкаДетальныеЗаписи.Артикул = Null Тогда
            ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи.Артикул));
        Иначе
            ЗаписьДж.ЗаписатьЗначение(""); // Пустая строка вместо Null
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.ЕдиницаИзмеренияНаименование = Null Тогда
            ЗаписьДж.ЗаписатьИмяСвойства("package");
            ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи.ЕдиницаИзмеренияНаименование));
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.ВидНоменклатурыНаименование = Null Тогда
            ЗаписьДж.ЗаписатьИмяСвойства("vid");
            ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи.ВидНоменклатурыНаименование));
        КонецЕсли;

        Если Не ВыборкаДетальныеЗаписи.СтавкаНДСНаименование = Null Тогда
            ЗаписьДж.ЗаписатьИмяСвойства("nds");
            ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи.СтавкаНДСНаименование));
        КонецЕсли;

        // Вложенные данные (остатки, цены) также можно записывать потоково.
        // Например, для остатков, если они представляют массив:
        // ЗаписьДж.ЗаписатьИмяСвойства("stocks");
        // ЗаписьДж.ЗаписатьНачалоМассива();
        // ... цикл по остаткам ...
        //     ЗаписьДж.ЗаписатьНачалоОбъекта();
        //     ЗаписьДж.ЗаписатьИмяСвойства("Количество");
        //     ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи1.ВНаличииОстаток));
        //     ЗаписьДж.ЗаписатьКонецОбъекта();
        // ...
        // ЗаписьДж.ЗаписатьКонецМассива();

        // Для упрощения примера, возьмем только первый найденный остаток/цену.
        Запрос1 = Новый Запрос;
        Запрос1.Текст = "ВЫБРАТЬ 
                       | ТоварыНаСкладахОстатки.Номенклатура.Ссылка КАК НоменклатураСсылка, 
                       | ТоварыНаСкладахОстатки.Склад.Наименование КАК СкладНаименование, 
                       | ТоварыНаСкладахОстатки.ВНаличииОстаток КАК ВНаличииОстаток 
                       |ИЗ 
                       | РегистрНакопления.ТоварыНаСкладах.Остатки КАК ТоварыНаСкладахОстатки 
                       |ГДЕ 
                       | ТоварыНаСкладахОстатки.Номенклатура.Ссылка = &Ссылка";
        Запрос1.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
        РезультатЗапроса1 = Запрос1.Выполнить();
        ВыборкаДетальныеЗаписи1 = РезультатЗапроса1.Выбрать();

        Если ВыборкаДетальныеЗаписи1.Следующий() Тогда
            Если Не ВыборкаДетальныеЗаписи1.ВНаличииОстаток = Null Тогда
                ЗаписьДж.ЗаписатьИмяСвойства("Ostatok");
                ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи1.ВНаличииОстаток));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи1.СкладНаименование = Null Тогда
                ЗаписьДж.ЗаписатьИмяСвойства("sklad");
                ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи1.СкладНаименование));
            КонецЕсли;
        КонецЕсли;

        Запрос2 = Новый Запрос;
        Запрос2.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 
                       | ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура, 
                       | ЦеныНоменклатурыСрезПоследних.ВидЦены.Наименование КАК ВидЦеныНаименование, 
                       | ЦеныНоменклатурыСрезПоследних.Цена КАК Цена, 
                       | ЦеныНоменклатурыСрезПоследних.Валюта.Наименование КАК ВалютаНаименование 
                       |ИЗ 
                       | РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних 
                       |ГДЕ 
                       | ЦеныНоменклатурыСрезПоследних.Номенклатура.Ссылка = &Ссылка 
                       | 
                       |УПОРЯДОЧИТЬ ПО 
                       | ЦеныНоменклатурыСрезПоследних.Период УБЫВ";
        Запрос2.УстановитьПараметр("Ссылка", ВыборкаДетальныеЗаписи.Ссылка);
        РезультатЗапроса2 = Запрос2.Выполнить();
        ВыборкаДетальныеЗаписи2 = РезультатЗапроса2.Выбрать();

        Если ВыборкаДетальныеЗаписи2.Следующий() Тогда
            Если Не ВыборкаДетальныеЗаписи2.Цена = Null Тогда
                ЗаписьДж.ЗаписатьИмяСвойства("price");
                ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи2.Цена));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи2.ВидЦеныНаименование = Null Тогда
                ЗаписьДж.ЗаписатьИмяСвойства("vidPrice");
                ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи2.ВидЦеныНаименование));
            КонецЕсли;
            Если Не ВыборкаДетальныеЗаписи2.ВалютаНаименование = Null Тогда
                ЗаписьДж.ЗаписатьИмяСвойства("Valuta");
                ЗаписьДж.ЗаписатьЗначение(Строка(ВыборкаДетальныеЗаписи2.ВалютаНаименование));
            КонецЕсли;
        КонецЕсли;
        
        ЗаписьДж.ЗаписатьКонецОбъекта(); // Завершаем текущий JSON-объект
    КонецЦикла;

    ЗаписьДж.ЗаписатьКонецМассива(); // Завершаем JSON-массив

    ЗаписьДж.Закрыть();
КонецФункции

Оба этих метода позволяют получить валидный JSON-массив. Выбор между ними зависит от объема данных и предпочтений разработчика. Для большинства стандартных задач, когда объем данных не экстремально велик, использование объекта Массив (первый способ) является более простым и понятным.

Дополнительные рекомендации

  1. Проверка на Null и пустые значения: Всегда проверяйте значения на Null перед их записью в JSON. Если значение Null, то лучше явно записать пустую строку "", число 0, или логическое значение Ложь (false), в зависимости от ожидаемого типа данных. Это предотвратит ошибки форматирования типа "key": ,.
  2. Вложенные структуры и массивы: Если в вашем JSON должны быть вложенные объекты или массивы, то внутри основной структуры (или при потоковой записи) вы также должны формировать соответствующие объекты Структура, Массив или использовать методы ЗаписатьНачалоОбъекта()/ЗаписатьКонецОбъекта() и ЗаписатьНачалоМассива()/ЗаписатьКонецМассива().
  3. Параметры записи JSON: Объект ПараметрыЗаписиJSON очень полезен для форматирования выходного файла JSON. Используйте его, чтобы сделать JSON-файл более читаемым, добавляя отступы (например, Символы.Таб) и переносы строк. Это значительно упрощает отладку.
  4. Функция JSONСтрока(): Если вам нужен JSON не в файле, а в виде строки (например, для передачи по HTTP), вы можете использовать глобальную функцию JSONСтрока(Значение, ПараметрыЗаписиJSON). Она принимает Структуру или Массив и возвращает их JSON-представление в виде строки.

Следуя этим рекомендациям и используя один из предложенных способов, вы сможете надежно формировать JSON-файлы любой сложности.

← На главную