Использование формата 1С JDTO в контексте идентичных конфигураций показало себя очень хорошо. Однако рано или поздно возникает вопрос обмена данными между конфигурациями, имеющими различия в метаданных. Для таких обменов было решено немного адаптировать формат 1С JDTO - "обезжирить" и кое-что причесать. В итоге получился формат DaJet JDTO.
Платформа 1С:Предприятие 8 имеет встроенный механизм сериализации/десериализации своих объектов в формат JSON.
Сериализация:
USD = Справочники.Валюты.НайтиПоКоду("840");
Объект = USD.ПолучитьОбъект();
ЗаписьJson = Новый ЗаписьJSON();
ЗаписьJson.УстановитьСтроку();
СериализаторXDTO.ЗаписатьJSON(ЗаписьJson, Объект, НазначениеТипаXML.Явное);
JSON = ЗаписьJson.Закрыть();
Получаемый таким образом JSON можно использовать для обмена данными, как между информационными базами 1С, так и для передачи во внешние системы.
Десериализация:
ЧтениеJSON = Новый ЧтениеJSON();
ЧтениеJSON.УстановитьСтроку(JSON);
ОбъектДанных = СериализаторXDTO.ПрочитатьJSON(ЧтениеJSON);
ОбъектДанных.ОбменДанными.Загрузка = Истина;
ОбъектДанных.Записать();
При этом следует учитывать, что при загрузке JDTO в информационную базу 1С, имеющую отличную от загружаемого JSON схему данных, может возникать три типа ошибок:
1. Отсутствие соответствующего типа данных (не критичная):
Ошибка при вызове метода контекста (ПрочитатьJSON): Неизвестный тип
2. Несоответствие схемы XDTO (не критичная):
Ошибка при вызове метода контекста (ПрочитатьJSON): Ошибка преобразования данных XDTO
3. Несоответствие схемы XDTO (критичная !!! - вылетает платформа):
Недопустимое значение аргумента функции: [xdto - src\XDTOFactoryImpl.cpp (414)]
Третья ошибка тянется, судя по поиску в Интернет, ещё с платформы 8.2. Была обнаружена на версии 8.3.18.1433. Возникает в том случае, если тип значения реквизита стал составным. При загрузке однозначного типа в составной возникает вот такая ошибка. Например, в моём случае реквизит имел значение одного перечисления, а затем к нему добавили ещё одно перечисление. Сериализатор XDTO поломался.
Важно также отметить, что порядок следования свойств объекта JDTO для 1С имеет значение.
Рассмотрим формат 1С JDTO (JSON data transfer object) более подробно на примере основных объектов 1С: перечислениях, справочниках, документах, регистрах сведений и накопления. Как правило в обмене данными с внешними системами используются именно они.
Согласно классификации объектов предметной области прикладного решения, которая была введена в широкую практику Мартином Фаулером и Эриком Эвансом, прикладные объекты 1С могут быть ссылочными (entity или reference object), значимыми (value object) и агрегатами (aggregate). Значимые типы данных далее будут называться табличными.
К ссылочным объектам в 1C относятся справочники и документы. Справочники и документы, имеющие табличные части, образуют агрегаты. Ссылочные объекты в 1С идентифицируются при помощи UUID (RFC 4122).
Справочники хранят данные о редко изменяемых сущностях прикладного решения. Например, это могут быть сведения о номенклатуре, контрагентах, местах хранения товара и т.п.
Документы фиксируют события, которые происходят со справочными сущностями, изменяя таким образом их учётное состояние в количественном или качественном выражении. Например, это может быть изменение остатка товара на складе или изменение его статуса.
Только ссылочные объекты могут иметь табличные части. В табличных частях хранится, связанная с данным объектом неразрывным образом информация, образуя в таком случае агрегат. Например, это может быть список товаров документа "Приходная накладная" или "Заказ покупателя".
Хорошей практикой в 1С считается хранение состояния сущностей отдельно от них самих. Для этого используются табличные типы данных (value object).
К табличным типам данных в 1С относятся регистры сведений и регистры накопления. Управление данными этих объектов осуществляется при помощи наборов записей, которые, как правило, имеют составной ключ.
Регистры сведений используются для хранения любой информации, которая связана с теми или иными ссылочными объектами прикладного решения. Регистры сведений бывают периодическими, подчинёнными регистратору и независимыми. Первый тип регистров сведений хранит информацию в разрезе дат и времени. Второй — в разрезе документов. Третий — настраивается произвольным образом согласно решаемым прикладным решением задачам.
Регистры накопления являются учётными регистрами и хранят информацию о состоянии сущностей в количественном выражении. Регистры накопления бывают двух типов: остатков и оборотов. Например, регистр остатков "Остатки товара на складах" будет хранить записи о приходах и расходах товаров, а регистр оборотов "Продажи товара" будет хранить движения выбытия товаров в результате их продажи.
Данные в регистрах накопления всегда связанны с тем или иным документом. Такие документы по отношению к регистрам накопления называются регистраторами.
Формат JDTO достаточно прост и интуитивно понятен. Рассмотрим пару примеров.
DTO типа справочник "Валюты":
{
"#type": "jcfg:CatalogObject.Валюты",
"#value":
{
"Ref": "9c556d4d-720f-11df-b436-0015e92f2802",
"DeletionMark": false,
"Code": "840",
"Description": "USD",
"НаименованиеПолное": "Доллар США"
}
}
DTO документа с табличной частью (массив "Товары"):
{
"#type": "jcfg:DocumentObject.ЗаказКлиента",
"#value": {
"Ref": "0227135d-296e-11e5-92f1-0050568b35ac",
"DeletionMark": false,
"Date": "2015-07-13T18:46:57",
"Number": "ТД00-000028",
"Posted": true,
"Контрагент": "2f5f7e5d-f873-11df-aecd-0015e9b8c48d",
"Валюта": "26093579-c180-11e4-a7a9-000d884fd00d",
"СуммаДокумента": 117625,
"Автор": {
"#type": "jcfg:CatalogRef.Пользователи",
"#value": "a4212b3d-730a-11df-b338-0011955cba6b"
},
"Товары": [
{
"Номенклатура": "bd72d927-55bc-11d9-848a-00112f43529a",
"Количество": 3,
"Цена": 6750,
"Сумма": 20250
}
]
}
}
Для обмена данными об удаляемых ссылочных объектах используется следующая сериализация JSON:
{
"#type": "jent:ObjectDeletion",
"#value": {
"Ref": {
"#type": "jcfg:CatalogRef.Валюты",
"#value": "9c556d4d-720f-11df-b436-0015e92f2802"
}
}
}
При этом все объекты 1С имеют, в зависимости от своего типа, предопределённые системные свойства. Это аналогично свойствам абстрактных классов в объектно-ориентированных языках программирования. Эти свойства наследуются всеми прикладными объектами 1С, производными от таких абстрактных классов.
Системные свойства справочников:
Имя |
Использование |
Тип данных JSON |
Описание |
Ref |
Обязательно |
string(uuid) |
Ссылка на данный объект в базе данных |
DeletionMark |
Обязательно |
boolean |
Признак удаления объекта. Фактическое удаление объекта может быть выполнено позже или никогда. |
Owner |
Используется только в том случае, если данный справочник имеет отношения владелец - подчинённый по отношению к другому справочнику |
string(uuid) |
Внешний ключ на объект другого справочника, который "владеет" данным объектом. Типов владельцев может быть несколько. |
Code |
Может не использоваться |
number string |
Код объекта, формируемый и назначаемый приложением 1С |
Description |
Может не использоваться | string |
Наименование объекта |
Parent |
Используется только в том случае, если данный справочник организован как иерархия элементов | string(uuid) |
Родительский объект (элемент этого же справочника) для данного объекта. Таким образом реализуется иерархия объектов одного и того же типа. |
IsFolder |
Используется только в том случае, когда справочник организован как иерархия групп и элементов | string(uuid) |
Вид родительского объекта: группа или объект (элемент). |
Системные свойства документов:
Имя | Использование | Тип данных JSON | Описание |
Ref | Обязательно | string(uuid) | Ссылка на данный объект в базе данных |
DeletionMark | Обязательно | boolean | Признак удаления объекта. Фактическое удаление объекта может быть выполнено позже или никогда. |
Date |
Обязательно |
string(date-time) ISO 8601 |
Дата и время документа |
Posted |
Обязательно | boolean |
Признак принятия документа к учёту |
Number |
Может не использоваться |
number string |
Номер документа, формируемый и назначаемый приложением 1С |
1С широко оперирует понятием "Ссылка". Ссылка - это указатель на ссылочный объект определённого типа данных. Идентификатором ссылки является UUID. Например, в документации по 1С можно часто увидеть описание ссылки таким образом: СправочникСсылка.Номенклатура. В выше приведённых примерах JSON ссылками являются, например, свойства "Валюта" и "Автор" документа "ЗаказКлиента".
В формате JDTO часто можно увидеть следующие имена для ссылочных типов данных:
jcfg:EnumRef.СпособОплаты — ссылка на значение перечисления "Способ оплаты"
jcfg:CatalogRef.Валюты – ссылка на элемент справочника "Валюты"
jcfg:DocumentRef.ЗаказКлиента — ссылка на документ "Заказ клиента"
Для объектов переноса данных (data transfer object) используются аналогичное именование:
jcfg:CatalogObject.Валюты – DTO справочника "Валюты"
jcfg:DocumentObject.ЗаказКлиента — DTO документа "Заказ клиента"
jent:ObjectDeletion – DTO удаления объекта (см. пример JSON выше)
Ссылки на объекты бывают "пустыми". Дело в том, что в 1С значением ссылки не может быть значение null. Вместо этого используется понятие "пустая ссылка". В формате JSON это выглядит так:
{
"Валюта": "00000000-0000-0000-0000-000000000000"
}
или так:
{
"Автор": {
"#type": "jcfg:CatalogRef.Пользователи",
"#value": "00000000-0000-0000-0000-000000000000"
}
}
Такая ссылка имеет тип данных и значение нулевого UUID.
1С поддерживает концепцию "составного типа данных". Значение такого типа может в каждый момент времени иметь разный тип данных. Например, оно может быть строкой, числом или ссылкой на объект.
Для сравнения в языке программирования C++ такой тип данных является экземпляром класса std::variant или объединением (union). В языке T-SQL это тип данных sql_variant.
Рассмотрим реализацию этой концепции подробнее. Как следует из примеров выше, значения ссылок могут кодироваться в JSON как простой UUID:
{
"Валюта": "26093579-c180-11e4-a7a9-000d884fd00d"
}
или как объект со свойствами #type и #value:
{
"Автор": {
"#type": "jcfg:CatalogRef.Пользователи",
"#value": "a4212b3d-730a-11df-b338-0011955cba6b"
}
}
В первом случае о типе значения остаётся только догадываться. Во втором случае очень просто понять какого типа ссылка закодирована в качестве значения свойства "Автор".
Это происходит потому, что при сериализации объекта в JDTO учитывает имеет ли данное свойство составной тип данных или одиночный. Во втором случае свойство "Автор" имеет составной тип данных 1С, а в первом — одиночный. В тех случаях, когда тип значения ссылки известен платформе 1С (для этого есть метаданные) и может быть только одного типа, платформа сериализует значение ссылки как простой UUID.
В первом случае при десериализации значения ссылки узнать её тип можно только из метаданных 1С. Для внешней системы эти метаданные можно выгрузить из 1С в виде XSD схемы. При помощи сторонних утилит можно по метаданным 1С генерировать JSON схему.
Таким образом, сериализация любого значения составного типа данных (примитивного или ссылочного) выглядит следующим образом:
{
"#type": "jcfg:CatalogRef.Пользователи",
"#value": "a4212b3d-730a-11df-b338-0011955cba6b"
}
Пример для примитивного строкового типа данных:
{
"#type": "jxs:string",
"#value": "это строковое значение"
}
Возможные значения свойств #type и #value:
#type |
#value |
jxs:string |
"строковое значение" |
jxs:decimal |
123 или 123.45 |
jxs:boolean |
true или false |
jxs:dateTime |
"2021-01-01T00:00:00" |
jcfg:EnumRef.ИмяПеречисления |
"НаличнаяОплата" (*) |
jcfg:CatalogRef.ИмяСправочника |
"a4212b3d-730a-11df-b338-0011955cba6b" |
jcfg:DocumentRef.ИмяДокумента |
"a4212b3d-730a-11df-b338-0011955cba6b" |
(*) Не смотря на то, что перечисления в 1С это ссылочные типы данных, значением перечисления в JDTO всегда является строка - имя для значения UUID в базе данных.
Составной тип данных является обнуляемым типом данных, то есть его значением может быть значение null. Например:
{
"Автор": null
}
DTO регистра сведений "КурсыВалют":
{
"#type": "jcfg:InformationRegisterRecordSet.КурсыВалют",
"#value": {
"Filter": [
{
"Name": {
"#type": "jxs:string",
"#value": "Валюта"
},
"Value": {
"#type": "jcfg:CatalogRef.Валюты",
"#value": "9c556d4d-720f-11df-b436-0015e92f2802"
}
}
],
"Record": [
{
"Period": "2021-07-13T00:00:00",
"Валюта": "9c556d4d-720f-11df-b436-0015e92f2802",
"Курс": 82.55
},
{
"Period": "2021-07-14T00:00:00",
"Валюта": "9c556d4d-720f-11df-b436-0015e92f2802",
"Курс": 81.3
},
{
"Period": "2021-07-15T00:00:00",
"Валюта": "9c556d4d-720f-11df-b436-0015e92f2802",
"Курс": 80.45
}
]
}
}
DTO регистра накопления "Расчёты с клиентами":
{
"#type": "jcfg:AccumulationRegisterRecordSet.РасчетыСКлиентами",
"#value": {
"Filter": [
{
"Name": {
"#type": "jxs:string",
"#value": "Recorder"
},
"Value": {
"#type": "jcfg:DocumentRef.ЗаказКлиента",
"#value": "0227135d-296e-11e5-92f1-0050568b35ac"
}
}
],
"Record": [
{
"Recorder": {
"#type": "jcfg:DocumentRef.ЗаказКлиента",
"#value": "0227135d-296e-11e5-92f1-0050568b35ac"
},
"Period": "2015-07-18T23:59:59",
"RecordType": "Receipt",
"Active": true,
"ЗаказКлиента": {
"#type": "jcfg:DocumentRef.ЗаказКлиента",
"#value": "0227135d-296e-11e5-92f1-0050568b35ac"
},
"Валюта": "26093579-c180-11e4-a7a9-000d884fd00d",
"Сумма": 0,
"ФормаОплаты": "ПлатежнаяКарта"
}
]
}
}
Системные свойства регистра сведений:
Имя |
Использование |
Тип данных JSON |
Описание |
Period |
Используется только для периодических регистров сведений. |
string(date-time) ISO 8601 |
Дата и время записи. |
Recorder |
Используется только для регистров сведений подчинённых регистратору. | string(uuid) |
Ссылка на документ, который выполнил регистрацию движений — создал данную запись. |
Active |
Используется только для регистров сведений подчинённых регистратору. | boolean |
Признак использования записи для учёта. Может использоваться, например, для разделения фискального и управленческого учёта. |
Системные свойства регистров накопления остатков и оборотов:
Имя | Использование | Тип данных JSON | Описание |
Period |
Обязательно |
string(date-time) ISO 8601 |
Дата и время записи. Как правило совпадает с датой документа. |
Recorder | Обязательно | string(uuid) |
Ссылка на документ, который выполнил регистрацию движений — создал данную запись. |
Active | Обязательно | boolean |
Признак использования записи для учёта. Может использоваться, например, для разделения фискального и управленческого учёта. |
RecordType |
Используется только для регистров накопления остатков. |
string (enum) |
"Receipt" — приход "Expense" – расход Не используется для регистров накопления оборотов. |
Как следует из примеров выше структура набора записей в формате JSON 1С выглядит следующим образом:
{
"#type": "jcfg:AccumulationRegisterRecordSet.ИмяРегистра",
"#value": {
"Filter": [ { /* ... */ } ],
"Record": [ { /* ... */ } ]
}
}
Свойство #type нам уже знакомо. Его использование в данном случае ровно такое же, как в случае ссылочных типов данных, то есть определение типа объекта переноса данных (data transfer object).
Это свойство может иметь следующий вид:
jcfg:InformationRegisterRecordSet.ИмяРегистраСведений
jcfg:AccumulationRegisterRecordSet.ИмяРегистраНакопления
Свойство #value содержит значение типа "набор записей".
Свойство Record это массив записей. Каждая запись это объект и её кодирование интуитивно понятно. Оно аналогично кодированию объектов ссылочного типа.
Свойство Filter это массив объектов типа "Элемент отбора". Каждый элемент отбора имеет свойства Name и Value. Свойство Name указывает на свойство записи, по которому выполняется отбор данных для набора записей. В свою очередь свойство Value содержит значение для соответствующего элемента отбора. По этому значению всегда выполняется фильтрация данных на равенство. Все элементы отбора объединяются между собой при помощи логического оператора AND.
При этом следует иметь ввиду, что свойство Filter может отсутствовать или иметь в качестве своего значения пустой массив. Это будет означать, что отбор не используется. То же самое можно сказать про свойство Record.
1С изменяет данные СУБД табличных типов данных при помощи наборов записей. Набор записей включает в себя отбор и сами записи. Алгоритм обработки набора записей платформой 1С выглядит следующим образом:
1. Выполняется удаление записей по отбору (DELETE).
2. Выполняется вставка записей (INSERT).
Соответственно, если отбор не используется, то удаляются все записи как если бы в выражении SQL DELETE не было бы указано предложение WHERE.
Кроме этого на уровне кода 1С набор записей может быть записан в СУБД, либо в режиме замещения (DELETE + INSERT), либо в режиме без замещения (только INSERT - отбор при этом игнорируется). Узнать в каком режиме была выполнена запись набора на уровне JDTO невозможно. Для этого необходимо описать объект JDTO дополнительно, например, поместив JDTO в тело сообщения, а дополнительную информацию в его заголовок.
Каждый табличный тип данных имеет уникальный составной ключ, идентифицирующий запись в таблице СУБД. Таким образом отбор может быть полным или неполным.
Полным отбором является такой отбор, который включает в себя значения для всех свойств ключа, то есть идентифицирует одну запись.
Неполным отбором является такой отбор, который устанавливает значения только для части свойств ключа, то есть потенциально идентифицирует несколько записей.
Если отбор используется в наборе записей, то его значения для соответствующих свойств записей набора должны быть идентичны.
Таким образом, в случае с табличными типами значений, в отличие от ссылочных, объект переноса данных типа "Удаление объекта" не используется (см. раздел № 1). Вместо этого используется отбор набора записей.
Для описания структур данных объектов прикладного решения 1С используются метаданные. Метаданные являются схемой данных прикладного решения. Частота изменения схемы данных зависит от интенсивности разработки прикладного решения.
1С имеет штатные средства для выгрузки схемы данных в формате XSD. Для выгрузки схемы данных в формате, например, JSON Schema требуется использование сторонних утилит.
Пример схемы XSD для документа "Заказ клиента":
<objectType name="DocumentObject.ЗаказКлиента">
<property name="Ref" type="DocumentRef.ЗаказКлиента"/>
<property name="DeletionMark" type="xs:boolean"/>
<property name="Date" type="xs:dateTime"/>
<property name="Number" type="xs:string"/>
<property name="Posted" type="xs:boolean"/>
<property name="Контрагент" type="CatalogRef.Контрагенты"/>
<property name="Валюта" type="CatalogRef.Валюты"/>
<property name="СуммаДокумента" type="xs:decimal"/>
<property name="Автор" nillable="true"/>
<property name="Товары"
type="DocumentTabularSectionRow.ЗаказКлиента.Товары"
lowerBound="0" upperBound="99999"/>
</objectType>
<objectType name="DocumentTabularSectionRow.ЗаказКлиента.Товары">
<property name="Номенклатура" type="CatalogRef.Номенклатура"/>
<property name="Количество" type="xs:decimal"/>
<property name="Цена" type="xs:decimal"/>
<property name="Сумма" type="xs:decimal"/>
</objectType>
Пример JSON Schema для документа "Заказ клиента":
{
"$id": "Документ.ЗаказКлиента",
"type": "object",
"additionalProperties": false,
"required": [ "#type", "#value" ],
"properties": {
"#type": { "const": "jcfg:DocumentObject.ЗаказКлиента" },
"#value": {
"type": "object",
"properties": {
"Ref": { "type": "string", "format": "uuid" },
"DeletionMark": { "type": "boolean" },
"Date": { "type": "string", "format": "date-time" },
"Number": { "type": "string", "maxLength": 9 },
"Posted": { "type": "boolean" },
"Контрагент": { "$ref": "Справочник.Контрагенты" },
"Валюта": { "$ref": "Справочник.Валюты" },
"СуммаДокумента": { "type": "number" },
"Автор": {
"oneOf": [
{ "type": "null" },
{ "type": "object",
"required": [ "#type", "#value" ],
"properties": {
"#type": { "type": "string" },
"#value": { "type": [ "string", "number", "boolean" ] }
}
}
]
},
"Товары": { "type": "array", "items": { "type": "object",
"properties": {
"Номенклатура": { "$ref": "Справочник.Номенклатура" },
"Количество": { "type": "number" },
"Цена": { "type": "number" },
"Сумма": { "type": "number" }
}
}
}
}
}
}
}
Часто использование 1С JDTO в чистом виде неудобно или нежелательно. Для трансформации JSON из одного формата в другой существует достаточно большое количество средств для различных программных платформ.
Например, при помощи библиотеки JUST.NET для C# можно трансформировать JDTO регистра сведений следующим образом. Используется техника аналогичная применению XSLT.
Исходный JDTO:
{
"#type": "jcfg:InformationRegisterRecordSet.ТестовыйРегистрСведений",
"#value": {
"Filter": [
{
"Name": {
"#type": "jxs:string",
"#value": "Recorder"
},
"Value": {
"#type": "jcfg:DocumentRef.ТестовыйДокумент",
"#value": "09738c52-e30c-11eb-9cac-1e086ba0b1a1"
}
},
{
"Name": {
"#type": "jxs:string",
"#value": "NumberValue"
},
"Value": {
"#type": "jxs:number",
"#value": 123.45
}
},
{
"Name": {
"#type": "jxs:string",
"#value": "BooleanValue"
},
"Value": {
"#type": "jxs:boolean",
"#value": true
}
}
],
"Record": [
{
"Recorder": {
"#type": "jcfg:DocumentRef.ТестовыйДокумент",
"#value": "09738c52-e30c-11eb-9cac-1e086ba0b1a1"
},
"Period": "2021-01-02T01:00:00",
"Active": true,
"Измерение1": "Значение измерения 1",
"Ресурс1": "Значение ресурса 1",
"Реквизит1": "Значение реквизита 1"
}
]
}
}
JSON трансформера для изменения исходного JDTO:
{
"Type": "#concat(РегистрСведений,#substring(#valueof($.#type),#lastindexof(#valueof($.#type),.),#subtract(#length(#valueof($.#type)),#lastindexof(#valueof($.#type),.))))",
"Filter": { "#loop($.#value.Filter)":
{
"Name": "#currentvalueatpath($.Name.#value)",
"Type": "#currentvalueatpath($.Value.#type)",
"Value": "#currentvalueatpath($.Value.#value)"
}
},
"Records": "#valueof($.#value.Record)"
}
Результат трансформации:
{
"Type": "РегистрСведений.ТестовыйРегистрСведений",
"Filter": [
{
"Name": "Recorder",
"Type": "jcfg:DocumentRef.ТестовыйДокумент",
"Value": "09738c52-e30c-11eb-9cac-1e086ba0b1a1"
},
{
"Name": "NumberValue",
"Type": "jxs:number",
"Value": 123.45
},
{
"Name": "BooleanValue",
"Type": "jxs:boolean",
"Value": true
}
],
"Records": [
{
"Recorder": {
"#type": "jcfg:DocumentRef.ТестовыйДокумент",
"#value": "09738c52-e30c-11eb-9cac-1e086ba0b1a1"
},
"Period": "2021-01-02T01:00:00",
"Active": true,
"Измерение1": "Значение измерения 1",
"Ресурс1": "Значение ресурса 1",
"Реквизит1": "Значение реквизита 1"
}
]
}
Как видно из примера, часть исходного JDTO, в котором описывался отбор набора записей (свойство Filter), стал значительно удобнее для восприятия и дальнейшего парсинга.
При этом следует отметить, что возможности трансформации JSON не ограничиваются выше приведённым примером.
Кроме этого возможна трансформация JSON, предоставленного внешней системой, в формат 1С JDTO для непосредственной загрузки объекта в базу данных 1С фактически без написания кода 1С.
На мой взгляд это очень гибкая и полезная возможность обработки JDTO.
Полезные ссылки:
JSON Schema - описание формата, примеры использования, инструменты и утилиты
JUST.NET - библиотека на языке C# для трансформации JSON аналогично XSLT