Работая с данными, мы постоянно сталкиваемся с необходимостью модифицировать их структуру для последующей обработки. Допустим, мы получили определенные данные в формате JSON из системы-отправителя, система-получатель же при этом принимает JSON, отличный от первого не по содержанию, но по форме: некоторые поля переименованы, иерархия элементов устроена иначе, присутствуют мелкие изменения структуры документа. Как быть в таком случае? Можно прибегнуть к программным методам Groovy, использовать JsonSlurper для парсинга первого документа и JsonBuilder для формирования второго с учетом изменений структуры (затронуты тут и тут). Но все эти операции кажутся излишними, если от нас не требуется проведения серьезных модификаций передаваемых данных. В таких случаях возникает желание работать с JSON’ом не императивно, а декларативно: описать некое правило трансформации JSON-to-JSON, которое будет «работать само», устраняя необходимость погружаться в написание рутинного кода, состоящего в большей степени не из сложной специфической работы с данными, а из вспомогательных операций извлечения и записи значений полей.
Именно такую возможность предоставляет java-библиотека JOLT, предназначенная для трансформации данных в формате JSON. Она позволяет нам описать цепочку правил (также в JSON-формате), в соответветствии с которыми должна быть изменена структура переданного документа.
Одно такое правило состоит из двух элементов: “operation”, представляющий собой идентификатор типа преобразования, и “spec”, содержащий внутри себя непосредственно то, какие действия необходимо совершить в рамках указанной операции. Вот некоторые из доступных преобразований:
- shift : копирование данных из входного дерева с перемещением в выходное дерево;
- default : применить к дереву значения по умолчанию;
- remove : удалить данные из дерева;
- sort : отсортировать значения в алфавитном порядке.
На примере описанное будет нагляднее. Допустим, у нас есть документ со списком объектов данных и с метаинформацией (как общей, так и для каждого отдельного объекта). А сам объект внутри себя содержит заголовок и список деталей. Рассмотрим подробнее три основных операции по изменению структуры передаваемых данных: удаление существующего поля; добавление нового (как фиксированной строки или числа); переименование полей и реорганизация структуры документа, то есть изменение иерархии элементов. Вот наш исходный JSON:
{
"metaInfo": {
"type": "tuneItExample"
},
"initialObjects": [
{
"initialHeader": {
"id": "header-1",
"number": "567"
},
"initialDetails": [
{
"id": "detail-1-1",
"detail_number": "46432",
"flag": 1
},
{
"id": "detail-1-2",
"detail_number": "84243",
"flag": 1
}
],
"metaInfo": {
"version": "1.0",
"lastModified": "2024-09-05",
"link": {
"href": "https://www.tune-it.ru/"
}
}
}
]
}
Используем для его преобразования следующее JOLT-правило:
[
{
"operation": "remove",
"spec": {
"metaInfo": "",
"initialObjects": {
"*": {
"metaInfo": ""
}
}
}
},
{
"operation": "shift",
"spec": {
"initialObjects": {
"*": {
"initialHeader": {
"number": "ResultingObjects[#3].Number"
},
"initialDetails": {
"*": {
"detail_number": "ResultingObjects[#4].ResultingDetails[#2].AdditionalField1.Value",
"flag": "ResultingObjects[#4].ResultingDetails[#2].&",
"@(2,initialHeader.id)": "ResultingObjects[#4].ResultingDetails[#2].AdditionalField2",
"id": "ResultingObjects[#4].ResultingDetails[#2].AdditionalField3"
}
}
}
}
}
},
{
"operation": "modify-default-beta",
"spec": {
"ResultingObjects": {
"*": {
"Description": "JOLT transformation result",
"ResultingDetails": {
"*": {
"AdditionalField4": "Converted detail"
}
}
}
}
}
}
]
Данная спецификация представляет собой список из трех правил, последовательно применяющихся к изменяемому документу.
В первой секции “remove” (строки 2-12 в JOLT) мы удаляем отдельные элементы и целые фрагменты нашего исходного JSON’а (синтаксически это делается через присваивание пустой строки, как в строке 5 в JOLT). Заметим особенности самой структуры элемента “spec”. Здесь мы сначала обращаемся к секции “metaInfo” на верхнем уровне иерархии (строки 2-4 в исходном JSON), далее переходим к списку initialObjects на том же уровне. После этого мы погружаемся “вглубь” списка объектов, и через “*” обращаемся к каждому элементу данного списка, внутри которого спускаемся еще ниже, обращаясь к “metaInfo”, расположенному уже внутри конкретного объекта (строки 23-29 в исходном JSON). Таким образом, с использованием JOLT мы удалили всю метаинформацию (элементы “metaInfo” с их содержимым) на каждом уровне иерархии. Следующие два преобразования будут применены исходя из версии документа с отствующей метаинформацией.
Секция “shift” (строки 13-32 в JOLT), которая следует далее, самая сложная для понимания. Здесь мы заново строим иерархию JSON-элементов, собираем новую структуру, исходя из текущей. Внутри “spec” мы последовательно проходимся по всем уровням иерархии исходного документа и по всем элементам списков (проходимся по “initialObjects“, “initialHeader“, “initialDetails“ и через “*“ обращаемся ко всем объектам списка). В процессе обхода для каждого поля мы указываем его место в новой структуре JSON.
Рассмотрим для примера строку 19. Все, что левее двоеточия, относится с исходному документу, то есть в данном случае к полю “number” внутри “initialHeader“ внутри элемента “initialObjects“. Правее от двоеточия показано, где значение этого поля должно быть в новом документе. Запись “ResultingObjects[#3].Number” означает, что “number” исходного документа должен стать “Number” в новом документе, а располагаться это поле должно внутри “ResultingObjects”. Запись “[#3]” можно трактовать как «на 3 уровня иерархии выше», то есть мы хотим, чтобы “ResultingObjects” стал аналогом “initialObjects”. Почему именно 3? Потому что считая от “number”, первым уровнем будет “initialHeader”, вторым “*” (уровень элементов списка), а третьим как раз будет “initialObjects”, который мы по сути и хотим переименовать. В итоге вложенность “initialObjects - элемент списка - initialHeader - number” превратится в “ResultingObjects - элемент списка - Number”.
Аналогичным образом это работает и с другими представленными преобразованиями, однако есть еще пара полезных деталей. Так, в строке 24 используется символ “&”, который обозначает “такое же название, как и в левой части”. То есть запись в строке эквивалентна записи "flag": "ResultingObjects[#4].ResultingDetails[#2].flag". В строке 25 также можно увидеть “@(2,initialHeader.id)”. Это означает, что мы хотим вернуться “наверх” и взять элемент “id” внутри “initialHeader”, находящийся на два уровня иерархии выше (опять же первый уровень - уровень элементов списка, второй - уровень “initialHeader” и “initialDetails”).
Финальной секций становится добавление полей (строки 33-47 в JOLT). Важно отметить, что здесь мы уже имеем дело с полностью переименованными полями (больше никаких initial). После операции “shift” здесь все становится достаточно просто. Мы добавляем поле "Description" к каждому объекту "ResultingObjects", а также поле “AdditionalField4” к каждому элементу подсписка "ResultingDetails".
После всех этих преобразований мы получим следующий JSON, поля которого переименованы, а структура изменена:
{
"ResultingObjects": [
{
"Number": "567",
"ResultingDetails": [
{
"AdditionalField2": "header-1",
"AdditionalField1": {
"Value": "46432"
},
"flag": 1,
"AdditionalField3": "detail-1-1",
"AdditionalField4": "Converted detail"
},
{
"AdditionalField2": "header-1",
"AdditionalField1": {
"Value": "84243"
},
"flag": 1,
"AdditionalField3": "detail-1-2",
"AdditionalField4": "Converted detail"
}
],
"Description": "JOLT transformation result"
}
]
}
Таким образом, JOLT становится мощным инструментом для формирования нового JSON’а на основе существующего. Аналогичным же образом можно вычленять из документа необходимую информацию, используя заданный набор правил. JOLT активно используется в Apche NiFi, работа с ним производится посредством отдельного процессора, однако сама библиотека может быть использована и в Java-приложениях. Кроме того, у JOLT есть удобная демоверсия, где можно тестировать трансформацию документов онлайн. Для более подробной информации следует также почитать документацию.
Главным недостатком становится отсутствие потоковой обработки, вследствие чего для успешной работы с большими документами должно быть выделено достаточно большое количество памяти (проверено на личном опыте, при использовании JOLT-процессора в Apache NiFi стандартных значений параметров Java heap space в настройках не хватало). Поэтому ценой за удобство и возможность работать с данными декларативно становятся затраты по памяти. Кроме того, как было сказано, все описанное ранее касается именно структурных изменений, включая добавление, переименование, удаление полей. Для осуществления вычислений и манипуляции данными по ходу работы с JSON использовать данную библиотеку на данный момент не представляется возможным.