Добрый день! Продолжаем тематику использования Jolt Transform. Возможно, кто-то уже ознакомился с моей предыдущей статьей на этут тему. Сегодня я попробую разобрать реализацию ещё одной задачи с помощью Apache NiFi и Jolt Transform.

Итак, задача с телеграмм-канала Powerful NiFi, участник dubrovski82 задал вопрос. Есть json:

{
  "Предупреждения" : {
    "ПРЕДУПРЕЖДЕНИЕ" : "Этот картриджей."
  },
  "Основные характеристики" : {
    "Назначение" : "Для печатающих устройств",
    "Производитель" : "Static Control",
    "Цвет чернил" : "Пурпурный (Magenta)",
    "Тип оборудования" : "Картридж",
    "описание" : "Великолепный картридж",
    "Модель" : "002-01-VF353A"
  }
}

Надо обратиться к последнему элементу общего списка, чтобы из него вытащить одно поле.

dubrovski82: Дело в том, что тут как раз задача стоит в том, чтобы вытянуть это поле "описание" из последнего блока в json файле.

Предполагается, что в файле будет, как минимум, один блок. И, соответственно, мне вначале надо определить последний блок, потом из него вытянуть "описание".
В приведенном примере выше всего два блока в файле: Предупреждения и Основные характеристики. И "последний" блок это "Основные характеристики". Для других файлов это могут быть другие названия.

Исходя из обсуждения было определено, что исходный json должен быть помещён в поле "isDescript"."details". А поле "описание" нужно перенести в поле "isDescript"."description". Итак, ожидаемый результат трансформации выглядит так:

{
  "isDescript": {
    "details": {
      "Предупреждения": {
        "ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
      },
      "Основные характеристики": {
        "Назначение": "Для печатающих устройств",
        "Производитель": "Static Control",
        "Цвет чернил": "Пурпурный (Magenta)",
        "Тип оборудования": "Картридж",
        "Модель": "002-01-VF353A"
      }
    },
    "description": "Великолепный картридж"
  }
}

Будем рассчитывать, что поле "описание" должно присутствовать в искомом блоке. Понятие "последний блок" для JSON несколько не специфично, поскольку стандарт не декларирует порядок "блоков". Иначе, всё сильно усложняется и задача может не иметь решения средствами Jolt Transform.

Добавляем в нашу процессорную группу процессор JoltTransformJSON

В настройках процессора пока ничего не меняем, сразу переходим в Properties -> Advanced

И помещаем в поле JSON Input наш исходный JSON

В поле Jolt Specification будем описывать спецификацию для трансформации нашего JSON

Для решения нашей задачи спецификация получается довольно простой. Думаю, что нет смысла описывать процесс написания, сразу приведу её и попробую выполнить.

[{
	"operation": "shift",
	"spec": {
		"*": {
			"описание": "isDescript.description",
			"*": "isDescript.details.&1.&"
		}
	}
}]

Проверяем, что оно работает.

Как мы видим, результат совпадает с ожидаемым.

{
	"isDescript": {
		"details": {
			"Предупреждения": {
				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
			},
			"Основные характеристики": {
				"Назначение": "Для печатающих устройств",
				"Производитель": "Static Control",
				"Цвет чернил": "Пурпурный (Magenta)",
				"Тип оборудования": "Картридж",
				"Модель": "002-01-VF353A"
			}
		},
		"description": "Великолепный картридж"
	}
}
Разверните, чтобы увидеть пояснение по данной спецификации

Итак, для поиска поля "описание" мы должны просканировать второй уровень исходного джейсона. Для этого мы просматриваем все элементы первого уровня, об этом говорит первая звёздочка "*": { ... }

"описание": "isDescript.description" - на втором уровне вложенности мы ищем поле "описание" и кладём его в результирующий json в поле isDescript.description.

"*": "isDescript.details.&1.&" - все остальные поля второго уровня кладём в те же поля в структуре первого уровня, но помещая саму структуру в поле isDescript.details

Это решение я и привел в качестве ответа на вопрос. Но, потом подумал...

Немного усложним задачу. Что, если на первом уровне появится поле, которое не содержит вложенных полей. Или простое поле, массив, пустой массив, или null.

{
	"Предупреждения": {
		"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
	},
	"Основные характеристики": {
		"Назначение": "Для печатающих устройств",
		"Производитель": "Static Control",
		"Цвет чернил": "Пурпурный (Magenta)",
		"Тип оборудования": "Картридж",
		"описание": "Великолепный картридж",
		"Модель": "002-01-VF353A"
	},
  
  	"Пустой объект": {},
    "Массив": [1,2,3],
    "Пустой массив": [],
  	"Что-то": "ещё",
    "Null": null  
}

К сожалению, наше решение не работает в таком случае. Результат удручает - некоторые поля потеряны, какие-то выглядят неожиданно:

{
	"isDescript": {
		"details": {
			"Предупреждения": {
				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
			},
			"Основные характеристики": {
				"Назначение": "Для печатающих устройств",
				"Производитель": "Static Control",
				"Цвет чернил": "Пурпурный (Magenta)",
				"Тип оборудования": "Картридж",
				"Модель": "002-01-VF353A"
			},
			"Массив": {
				"0": 1,
				"1": 2,
				"2": 3
			},
			"Что-то": {
				"ещё": null
			}
		},
		"description": "Великолепный картридж"
	}
}

Будем исправлять, и.... Кажется, нам просто надо взять другой шаблон (подстановочный знак) вместо "*" - "@"

@ - шаблон, который необходим, если вы хотите поместить как входное значение, так и входной ключ куда-нибудь в выходной JSON. Но, если мы напишем просто:

[{
	"operation": "shift",
	"spec": {
		"*": {
			"описание": "isDescript.description",
			"@": "isDescript.details"
		}
	}
}]

то получим следующее:

{
	"isDescript": {
		"details": [{
				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
			}, {
				"Назначение": "Для печатающих устройств",
				"Производитель": "Static Control",
				"Цвет чернил": "Пурпурный (Magenta)",
				"Тип оборудования": "Картридж",
				"описание": "Великолепный картридж",
				"Модель": "002-01-VF353A"
			}, {},
			[1, 2, 3],
			[], "ещё", null
		],
		"description": "Великолепный картридж"
	}
}

Мы потеряли ключи, надо их вернуть, для этого вернём подстановочный знак & в правую часть:

[{
	"operation": "shift",
	"spec": {
		"*": {
			"описание": "isDescript.description",
			"@": "isDescript.details.&"
		}
	}
}]

& - подстановочный знак, в правой части он обычно используется совместно со знаком "*" в левой части. Полная форма - "&(0,0)".

Первый параметр - это то, где во входном пути искать значение (на каком уровне):

{
   "foo" : {
     "bar": {
       "baz":  // &0 = baz, &1 = bar, &2 = foo
     }
   }
 }

Второй параметр - указывает на ту часть ключа, которую надо использовать:

// Если у нас есть исходный ключ "tag-Foo-Bar" 
"tag-*-*": "&(0,0)" = "tag-Foo-Bar"
"tag-*-*": "&(0,1)" = "Foo"
"tag-*-*": "&(0,2)" = "Bar"

Так как у нас в левой части стоит шаблон "@", то использование второго параметра, отличного от 0 будет приводить к ошибке. А вот с первым будет несколько недетерминированное проведение, а точнее "&" = "&0", что как бы очевидно, но тут еще и "&" = "&0" = "&1" (для "&2" всё ок, как ожидается, на наших входных данных это - root). Скорее всего это вызвано тем, что @ берёт ключ и значение (как бы два уровня), а & всегда означает ключ. Но, нам нет необходимости, в нашем случае, указывать параметры для шаблона.

Финальная спецификация:

[{
	"operation": "shift",
	"spec": {
		"*": {
			"описание": "isDescript.description",
			"@": "isDescript.details.&"
		}
	}
}]

Итак, мы получили в результате:

{
	"isDescript": {
		"details": {
			"Предупреждения": {
				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
			},
			"Основные характеристики": {
				"Назначение": "Для печатающих устройств",
				"Производитель": "Static Control",
				"Цвет чернил": "Пурпурный (Magenta)",
				"Тип оборудования": "Картридж",
				"описание": "Великолепный картридж",
				"Модель": "002-01-VF353A"
			},
			"Пустой объект": {},
			"Массив": [1, 2, 3],
			"Пустой массив": [],
			"Что-то": "ещё",
			"Null": null
		},
		"description": "Великолепный картридж"
	}
}

Комментарии (9)


  1. BogdanPetrov
    14.11.2022 15:13

    С Jolt не работал и честно говоря не понимаю, как здесь соблюдается требование, чтобы поле "описание" вытягивалось именно из "последнего" (я так предполагаю, здесь как и в jq порядок ключей сохраняется) блока?

    [{
    	"operation": "shift",
    	"spec": {
    		"*": {
    			"описание": "isDescript.description",
    			"@": "isDescript.details.&"
    		}
    	}
    }]


    1. BogdanPetrov
      14.11.2022 15:23

      А, увидел примечание в статье

      Будем рассчитывать, что поле "описание" должно присутствовать в искомом блоке. Понятие "последний блок" для JSON несколько не специфично, поскольку стандарт не декларирует порядок "блоков". Иначе, всё сильно усложняется и задача может не иметь решения средствами Jolt Transform.

      В таком случае, что будет если поле "описание" будет присутствовать в нескольких "блоках"?


      1. kxl Автор
        14.11.2022 15:37
        +1

        Эта ситуация маловероятна со слов автора вопроса. Но, если взять эту-же спецификацию и пример

        {
          "Предупреждения" : {
            "ПРЕДУПРЕЖДЕНИЕ" : "Этот картриджей."
          },
          "Основные характеристики" : {
            "Назначение" : "Для печатающих устройств",
            "Производитель" : "Static Control",
            "Цвет чернил" : "Пурпурный (Magenta)",
            "Тип оборудования" : "Картридж",
            "описание" : "Великолепный картридж",
            "Модель" : "002-01-VF353A"
          },
          "Другие характеристики" : {
            "описание" : "плохой картридж",
            "годен до" : "002-01-VF353A"
          }
        }

        Получится:

        {
        	"isDescript": {
        		"details": {
        			"Предупреждения": {
        				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
        			},
        			"Основные характеристики": {
        				"Назначение": "Для печатающих устройств",
        				"Производитель": "Static Control",
        				"Цвет чернил": "Пурпурный (Magenta)",
        				"Тип оборудования": "Картридж",
        				"описание": "Великолепный картридж",
        				"Модель": "002-01-VF353A"
        			},
        			"Другие характеристики": {
        				"описание": "плохой картридж",
        				"годен до": "002-01-VF353A"
        			}
        		},
        		"description": ["Великолепный картридж", "плохой картридж"]
        	}
        }


        1. kxl Автор
          14.11.2022 15:42
          +1

          Тип description сменился на массив, а это может быть не очень хорошо для принимающей стороны. Если вы уверены, что такие данные будут вам попадаться, то можно description принудительно сделать массивом.

          [{
          	"operation": "shift",
          	"spec": {
          		"*": {
          			"описание": "isDescript.description[]",
          			"@": "isDescript.details.&"
          		}
          	}
          }]


        1. BogdanPetrov
          14.11.2022 15:50

          Так и думал, спасибо

          NiFi под рукой нет, но нашел песочницу, в которой можно прогнать и получить этот результат: https://jolt-demo.appspot.com

          Вообще, тема довольно интересная, но получается специфика NiFi не раскрыта, больше про Jolt.

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


          1. kxl Автор
            14.11.2022 16:04
            +1

            Да, можно.... Например, будет такая цепочка процессоров.

            В RouteOnAttribute проверять размерность массива


    1. kxl Автор
      14.11.2022 15:27
      +1

      Именно из "последнего" - никак... об этом есть в ремарка в тексте.

      По итогам обсуждения задачи стало понятно, что искать "последнее" не требуется. Там посыл был такой - ожидается, что поле "описание" будет в каждом файле и именно в последнем блоке, даже если там блок один.

      Т.е. будет один блок с полем "описание". А вот в названии ключа этого блока, который тут - "основные характеристики" могло быть всё, что угодно.

      Если было бы несколько "описаний" и брать именно из последнего, то задача сильно усложнится...


    1. kxl Автор
      14.11.2022 20:46
      +1

      В уже упомянутом телеграмм-канале подсказали, что я заблуждаюсь насчет последнего элемента. И если подумать, то вырисовывается такое решение:

      [{
      	"operation": "shift",
      	"spec": {
      		"@": ["isDescript.details", "arr"]
      	}
      }, {
      	"operation": "shift",
      	"spec": {
      		"arr": {
      			"*": "arr[]"
      		},
      		"*": "&"
      	}
      }, {
      	"operation": "modify-overwrite-beta",
      	"spec": {
      		"arr": "=lastElement(@(1,arr))"
      	}
      }, {
      	"operation": "shift",
      	"spec": {
      		"arr": {
      			"описание": "isDescript.description"
      		},
      		"*": "&"
      	}
      }]

      которое даёт

      {
      	"isDescript": {
      		"details": {
      			"Предупреждения": {
      				"ПРЕДУПРЕЖДЕНИЕ": "Этот картриджей."
      			},
      			"Основные характеристики": {
      				"Назначение": "Для печатающих устройств",
      				"Производитель": "Static Control",
      				"Цвет чернил": "Пурпурный (Magenta)",
      				"Тип оборудования": "Картридж",
      				"описание": "Великолепный картридж",
      				"Модель": "002-01-VF353A"
      			},
      			"Другие характеристики": {
      				"описание": "Плохой картридж",
      				"Модель": "002-01-VF353A"
      			}
      		},
      		"description": "Плохой картридж"
      	}
      }


      1. kxl Автор
        14.11.2022 22:20
        +1

        хотя, лучше даже будет охватить такой вариант, когда последний блок не содержит "описание" и нужно получить последний из тех, что всё-таки содержат:

        [{
        	"operation": "shift",
        	"spec": {
        		"@": ["isDescript.details", "arr"]
        	}
        }, {
        	"operation": "shift",
        	"spec": {
        		"arr": {
        			"*": "arr[]"
        		},
        		"*": "&"
        	}
        }, {
        	"operation": "shift",
        	"spec": {
        		"arr": {
        			"*": {
        				"описание": "arr"
        			}
        		},
        		"*": "&"
        	}
        }, {
        	"operation": "modify-overwrite-beta",
        	"spec": {
        		"arr": "=lastElement(@(1,arr))"
        	}
        }, {
        	"operation": "shift",
        	"spec": {
        		"arr": "isDescript.description",
        		"*": "&"
        	}
        }]