null

JOLT - инструмент для JSON-to-JSON трансформаций

Работая с данными, мы постоянно сталкиваемся с необходимостью модифицировать их структуру для последующей обработки. Допустим, мы получили определенные данные в формате 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 использовать данную библиотеку на данный момент не представляется возможным.