Обмен данными в формате JSON очень популярен. То же самое можно сказать про организацию обменов при помощи КД-2 и, например, обработки "УниверсальныйОбменДаннымиXML". Однако "из коробки" эти инструменты не поддерживают обмен данными в формате JSON.
Формулировка задачи:
1. Реализовать пообъектный обмен данными: один объект конфигурации — одно сообщение в формате JSON.
2. Требуется использовать правила конвертации КД-2.
3. Требуется использовать типовую обработку "УниверсальныйОбменДаннымиXML".
4. Типовой функционал должен остаться прежним.
Основная идея реализации:
Опционально перехватывать запись XML и вместо него выполнять запись JSON. Для этого предлагается сделать обработку "СериализаторJSON", которая бы переопределяла методы объекта "ЗаписьXML", и "подсунуть" её там, где это необходимо. Фактически это позволяет "на лету" генерировать JSON из XML.
Пример использования для документа
Сериализатор = Обработки.СериализаторJSON.Создать();
Обработка = Обработки.УниверсальныйОбменДаннымиXML.Создать();
Обработка.РежимОбмена = "Выгрузка";
Обработка.СериализаторJSON = Сериализатор;
Обработка.ИмяФайлаПравилОбмена = "C:\temp\ПравилаОбменаJSON.xml";
Обработка.ЗагрузитьПравилаОбмена();
Док = Документы.ЗаказКлиента.НайтиПоНомеру("0001", ТекущаяДата());
Обработка.ВыгрузитьПоПравилу(Док,,,,"ЗаказКлиента");
Сообщение = Обработка.СериализаторJSON.Результат();
Сообщить(Сообщение.ТипСообщения); // ДокументСсылка.Заказ
Сообщить(Сообщение.ТелоСообщения); // Сформированный JSON
Пример использования для регистра сведений
Сериализатор = Обработки.СериализаторJSON.Создать();
Обработка = Обработки.УниверсальныйОбменДаннымиXML.Создать();
Обработка.РежимОбмена = "Выгрузка";
Обработка.СериализаторJSON = Сериализатор;
Обработка.ИмяФайлаПравилОбмена = "C:\temp\ПравилаОбменаJSON.xml";
Обработка.ЗагрузитьПравилаОбмена();
Товар = Справочники.Номенклатура.НайтиПоКоду("0001");
Набор = РегистрыСведений.ЦеныНоменклатуры.СоздатьНаборЗаписей();
Набор.Отбор.Номенклатура.Установить(Товар);
Набор.Прочитать();
Для Каждого Запись Из Набор Цикл
Обработка.ВыгрузитьПоПравилу(Запись,,,,"ЦеныНоменклатуры");
Сообщение = Обработка.СериализаторJSON.Результат();
Сообщить(Сообщение.ТипСообщения); // РегистрСведенийЗапись.ЦеныНоменклатуры
Сообщить(Сообщение.ТелоСообщения); // Сформированный JSON
КонецЦикла;
Изменение обработки "УниверсальныйОбменДаннымиXML"
1. Добавляем в обработку реквизит "СериализаторJSON" с типом значения "Произвольный".
2. Меняем код функции "СоздатьУзел" модуля объекта (обработки):
Функция СоздатьУзел(Имя) Экспорт
Если СериализаторJSON <> Неопределено Тогда
ЗаписьXML = СериализаторJSON; // "Подсовываем" свой сериализатор
Иначе
ЗаписьXML = Новый ЗаписьXML;
КонецЕсли;
ЗаписьXML.УстановитьСтроку();
ЗаписьXML.ЗаписатьНачалоЭлемента(Имя);
Возврат ЗаписьXML;
КонецФункции
3. Блокируем вывод в файл — меняем код процедуры "ЗаписатьВФайл":
Если Не ПустаяСтрока(ИмяФайлаОбмена) Тогда
ФайлОбмена.ЗаписатьСтроку(ИнформацияДляЗаписиВФайл);
КонецЕсли;
4. Добавляем принудительную инициализацию переменных модуля обработки:
мСчетчикНПП = 0;
мСчетчикВыгруженныхОбъектов = 0;
ФлагКомментироватьОбработкуОбъектов = Ложь;
ТекущийУровеньВложенностиВыгрузитьПоПравилу = 0;
Последний пункт необходим потому, что используемая функция "ВыгрузитьПоПравилу" будет вызваться в некотором смысле вне контекста обработки "УниверсальныйОбменДаннымиXML", без вызова метода "ВыполнитьВыгрузку". Таким образом выше упомянутые переменные модуля не будут инициализированы и вызов функции "ВыгрузитьПоПравилу" будет вызывать исключения. Для наглядности приведу скриншот модуля обработки:
Нюансы настройки ПКО (правил конвертации объектов)
Пообъектный обмен данными в формате JSON, как правило, использует выгрузку всех ссылочных объектов только по их ссылкам. Это называется "поверхностное копирование" (shallow copy), что противоположно "глубокому копированию" (deep copy). Другой вариант я даже не рассматривал - в предлагаемой реализации может не работать.
Для того, чтобы избежать рекурсивной выгрузки объектов необходимо выполнить следующие настройки ПКО.
1. Эта настройка позволяет выгружать стандартный реквизит "Ссылка" ссылочного объекта.
2. Следующая настройка предотвращает рекурсивную выгрузку ссылочных объектов.
Настройка "Не запоминать выгруженные объекты" в данном случае скорее нужна для оптимизации, чем для чего-то ещё.
При пообъектной выгрузке механизм отслеживания уже выгруженных объектов не нужен - лишние проверки.
Опорная информация, необходимая для реализации
Для того, чтобы конвертировать XML в JSON необходимо знать схему этого самого XML.
<Объект Нпп="1" Тип="ДокументСсылка.Заказ" ИмяПравила="ЗаказКлиента">
<Ссылка>
<Свойство Имя="{УникальныйИдентификатор}" Тип="Строка">
<Значение>a01f51e4-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
</Ссылка>
<Свойство Имя="Дата" Тип="Дата">
<Значение>2022-06-28T00:10:53</Значение>
</Свойство>
<Свойство Имя="Номер" Тип="Строка">
<Значение>000000001</Значение>
</Свойство>
<Свойство Имя="ПометкаУдаления" Тип="Булево">
<Значение>false</Значение>
</Свойство>
<Свойство Имя="Проведен" Тип="Булево">
<Значение>true</Значение>
</Свойство>
<Свойство Имя="Клиент" Тип="СправочникСсылка.Клиенты">
<Ссылка>
<Свойство Имя="{УникальныйИдентификатор}" Тип="Строка">
<Значение>7158f6b0-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
</Ссылка>
</Свойство>
<ТабличнаяЧасть Имя="Состав">
<Запись>
<Свойство Имя="Товар" Тип="Строка">
<Значение>786b6cfa-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
<Свойство Имя="Количество" Тип="Число">
<Значение>10</Значение>
</Свойство>
<Свойство Имя="Цена" Тип="Число">
<Значение>10</Значение>
</Свойство>
</Запись>
<Запись>
<Свойство Имя="Товар" Тип="Строка">
<Значение>7f39e845-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
<Свойство Имя="Количество" Тип="Число">
<Значение>20</Значение>
</Свойство>
<Свойство Имя="Цена" Тип="Число">
<Значение>20</Значение>
</Свойство>
</Запись>
<Запись>
<Свойство Имя="Товар" Тип="Строка">
<Значение>7f3a0b1a-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
<Свойство Имя="Количество" Тип="Число">
<Значение>30</Значение>
</Свойство>
<Свойство Имя="Цена" Тип="Число">
<Значение>30</Значение>
</Свойство>
</Запись>
</ТабличнаяЧасть>
</Объект>
Пример записи регистра сведений в XML
<Объект Нпп="1" Тип="РегистрСведенийЗапись.ЦеныНоменклатуры" ИмяПравила="ЦеныНоменклатуры">
<Свойство Имя="Активность" Тип="Булево">
<Пусто/>
</Свойство>
<Свойство Имя="Номенклатура" Тип="Строка">
<Значение>786b6cfa-f65d-11ec-9ccf-408d5c93cc8e</Значение>
</Свойство>
<Свойство Имя="Период" Тип="Дата">
<Значение>2022-06-28T00:00:00</Значение>
</Свойство>
<Свойство Имя="Цена" Тип="Число">
<Значение>10</Значение>
</Свойство>
</Объект>
Таким образом получаем список тех элементов и атрибутов XML, которые мы хотим обрабатывать:
Объект - объект конфигурации
Свойство - свойство объекта конфигурации (реквизит, измерение, ресурс)
Значение - значение свойства объекта конфигурации
Ссылка - значение свойства объекта ссылочного типа
Пусто - значение свойства объекта "Неопределено"
ТабличнаяЧасть - табличная часть объекта конфигурации
Запись - запись табличной части
{УникальныйИдентификатор} - имя стандартного свойства "Ссылка"
Реализация обработки "СериализаторJSON"
Изменения вносим только в модуль объекта обработки "СериализаторJSON":
Управление стэком прочитанных элементов XML
// Добавляет только что прочитанный элемент XML на стэк
Процедура ДобавитьЭлемент(ТипЭлемента)
Элемент = Новый Структура();
Элемент.Вставить("ТипЭлемента", ТипЭлемента);
_СтэкЭлементовXML.Добавить(Элемент);
_ТекущийИндекс = _ТекущийИндекс + 1;
КонецПроцедуры
// Возвращает последний прочитанный элемент XML
Функция ТекущийЭлемент()
Возврат _СтэкЭлементовXML[_ТекущийИндекс];
КонецФункции
// Возвращает и удаляет со стэка последний прочитанный элемент XML
Функция ПолучитьЭлемент()
Элемент = _СтэкЭлементовXML[_ТекущийИндекс];
_СтэкЭлементовXML.Удалить(_ТекущийИндекс);
_ТекущийИндекс = _ТекущийИндекс - 1;
Возврат Элемент;
КонецФункции
// Возвращает текущий элемент "Свойство",
// в контексте которого мы находимся сейчас
Функция ТекущееСвойство()
Элемент = Неопределено;
Индекс = _ТекущийИндекс;
Если Индекс < 0 Тогда
Возврат Элемент;
КонецЕсли;
Пока Индекс >= 0 Цикл
Элемент = _СтэкЭлементовXML[Индекс];
Если Элемент.ТипЭлемента = "Свойство" Тогда
Прервать;
КонецЕсли;
Индекс = Индекс - 1;
КонецЦикла;
Возврат Элемент;
КонецФункции
Запись XML и его трансформация "на лету" в JSON
Процедура ЗаписатьНачалоЭлемента(Имя) Экспорт
ДобавитьЭлемент(Имя);
Попытка
Если Имя = "Объект" Тогда // Объект конфигурации приёмника
_ЗаписьJSON = Новый ЗаписьJSON();
_ЗаписьJSON.УстановитьСтроку(Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет, ""));
_ЗаписьJSON.ЗаписатьНачалоОбъекта();
Если Не ПустаяСтрока(_Ссылка) Тогда
// XML ссылки формируется до начала записи объекта
_ЗаписьJSON.ЗаписатьИмяСвойства("Ссылка");
_ЗаписьJSON.ЗаписатьЗначение(_Ссылка);
_Ссылка = "";
КонецЕсли;
ИначеЕсли Имя = "Запись" Тогда // Запись табличной части
_ЗаписьJSON.ЗаписатьНачалоОбъекта();
Иначе
// Неподдерживаемый элемент XML
КонецЕсли;
Исключение
ТекстОшибки = ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Процедура ЗаписатьАтрибут(Имя, Значение) Экспорт
Попытка
Элемент = ТекущийЭлемент();
// Запоминаем значения атрибутов элементов XML
Элемент.Вставить(Имя, Значение);
Если Имя = "Имя" И Элемент.ТипЭлемента = "ТабличнаяЧасть" Тогда
_ЗаписьJSON.ЗаписатьИмяСвойства(Значение);
_ЗаписьJSON.ЗаписатьНачалоМассива();
КонецЕсли;
Исключение
ТекстОшибки = ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Процедура ЗаписатьТекст(Текст) Экспорт
// Запись значения свойства объекта конфигурации
Попытка
Элемент = ТекущийЭлемент();
Если ТекущийЭлемент().ТипЭлемента = "Значение" Тогда
Элемент.Вставить("Значение", Текст);
КонецЕсли;
Исключение
ТекстОшибки = ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Процедура ЗаписатьКонецЭлемента() Экспорт
// Основная процедура формирования JSON из событий записи XML
Попытка
Элемент = ПолучитьЭлемент();
Если Элемент.ТипЭлемента = "Объект" Тогда
_ЗаписьJSON.ЗаписатьКонецОбъекта();
_TYPE = Элемент.Тип;
_JSON = _ЗаписьJSON.Закрыть();
ИначеЕсли Элемент.ТипЭлемента = "Свойство" Тогда
Если Элемент.Имя <> "{УникальныйИдентификатор}" Тогда
_ЗаписьJSON.ЗаписатьИмяСвойства(Элемент.Имя);
_ЗаписьJSON.ЗаписатьЗначение(Элемент.Значение);
КонецЕсли;
ИначеЕсли Элемент.ТипЭлемента = "Значение" Тогда
Свойство = ТекущееСвойство();
Если Свойство <> Неопределено Тогда
Если Свойство.Имя = "{УникальныйИдентификатор}" Тогда
_Ссылка = Элемент.Значение;
ИначеЕсли Не Свойство.Свойство("Значение") Тогда
Свойство.Вставить("Значение", Элемент.Значение);
КонецЕсли;
КонецЕсли;
ИначеЕсли Элемент.ТипЭлемента = "Ссылка" Тогда
Свойство = ТекущееСвойство();
Если Свойство <> Неопределено Тогда
Свойство.Вставить("Значение", _Ссылка);
КонецЕсли;
ИначеЕсли Элемент.ТипЭлемента = "Пусто" Тогда
Свойство = ТекущееСвойство();
Если Свойство <> Неопределено Тогда
Свойство.Вставить("Значение", Неопределено);
КонецЕсли;
ИначеЕсли Элемент.ТипЭлемента = "ТабличнаяЧасть" Тогда
_ЗаписьJSON.ЗаписатьКонецМассива();
ИначеЕсли Элемент.ТипЭлемента = "Запись" Тогда
_ЗаписьJSON.ЗаписатьКонецОбъекта();
КонецЕсли;
Исключение
ТекстОшибки = ОписаниеОшибки();
КонецПопытки;
КонецПроцедуры
Процедуры, которые нужны, но делаем "пустышками"
Процедура УстановитьСтроку(ТипКодировки = Неопределено) Экспорт
// Ничего не делаем
КонецПроцедуры
Процедура ЗаписатьБезОбработки(Текст) Экспорт
// Ничего не делаем
КонецПроцедуры
Функция Закрыть() Экспорт
Возврат ""; // Ничего не делаем
КонецФункции
Основная логика обработки находится в процедуре "ЗаписатьКонецЭлемента", так как к этому моменту все значения, связанные с этим элментом, уже получены.
{
"Ссылка": "a01f51e4-f65d-11ec-9ccf-408d5c93cc8e",
"Дата": "2022-06-28T00:10:53",
"Номер": "000000001",
"ПометкаУдаления": "false",
"Проведен": "true",
"Клиент": "7158f6b0-f65d-11ec-9ccf-408d5c93cc8e",
"Состав": [
{
"Товар": "786b6cfa-f65d-11ec-9ccf-408d5c93cc8e",
"Количество": "10",
"Цена": "10"
},
{
"Товар": "7f39e845-f65d-11ec-9ccf-408d5c93cc8e",
"Количество": "20",
"Цена": "20"
},
{
"Товар": "7f3a0b1a-f65d-11ec-9ccf-408d5c93cc8e",
"Количество": "30",
"Цена": "30"
}
]
}
[
{
"Активность": "true",
"Номенклатура": "786b6cfa-f65d-11ec-9ccf-408d5c93cc8e",
"Период": "2022-06-28T00:00:00",
"Цена": "10"
},
{
"Активность": "true",
"Номенклатура": "786b6cfa-f65d-11ec-9ccf-408d5c93cc8e",
"Период": "2022-06-29T00:00:00",
"Цена": "123"
}
]
Итоговые замечания:
1. Все обработчики КД-2, которые будут настроены, отработают для ПКО, ПКС и т.п. Весь типовой функционал отработает так, как это реализовано в обработке "УниверсальныйОбменДаннымиXML". Перехватывается только вывод в XML.
2. Загрузка данных в приёмнике является достаточно простой задачей, так как тот JSON, который формируется в итоге, фактически является родным форматом для принимающей стороны. По этой причине в данной статье вопрос реализации загрузки не рассматривается.
3. Загрузка значений составного типа потребует немного изменить формат выходного JSON, например, так:
{
"СвойствоСоставногоТипа":
{
"type": "СправочникСсылка.Номенклатура",
"value": "786b6cfa-f65d-11ec-9ccf-408d5c93cc8e"
}
}
Дополнительно доработать это можно в процедуре "ЗаписатьКонецЭлемента", где выполняется запись в JSON свойства объекта и его значения. Элемент XML в этот момент времени уже содержит необходимое для этого значение своего атрибута "Тип".
4. Описанный подход допускает "докрутить" обработки таким образом, чтобы их можно было встраивать в конвейеры обработки сообщений (см. статью по ссылке).
5. Работа обработки "СериализаторJSON" проверялась с наиболее распространёнными объектами конфигураций такими, как справочники, документы, регистры сведений. При работе с другими объектами могут возникнуть нюансы сериализации из-за присутствия каких-нибудь специфических тэгов XML, которые в текущей реализации просто напросто не обрабатываются. Это можно доработать, как говорится "по месту".
Обработка тестировалась на платформе 8.3.20.1674.