There are 3 pieces of a transformation:
- input: The source
- spec: The strategy for transforming the input
- output: The result of the transformation.
So output = spec(input)
.
Jolt, serves a similar use case. However, Jolt is a FULL featured transformer. If Jolt is a helicopter, then j2j4J is a go cart. It is much faster and more agile if you are just driving around the neighborhood. This interface aims to be different from Jolt by
- Providing a more simple interface with a smaller learning curve
- Chain transformations at field level instead of multiple transformations of the entire object
- Adopts Angular's "Template Pipe Operator" syntax for transformations
- Adopts Spring default value syntax.
- Serve more simple use cases
- Does NOT support dynamic structure base on input i.e. no dynamic key names.
- No support for object => array or vice-versa.
- Be output focucused instead of input focused
- Jolt transformations are structured/defined by the input structure
- j2j4J transformations are defined/structured by output
- Easier maitenance
- Ability to diff across different input structures
- Easily extendible
- Be completely configuration based
- This including the ability to add custom transformations through configurations (embedded scripting)
Define the structure in the mapping specification. The output will have the same structure as the specification.
SPEC: OUTPUT:
{ | {
"id": ..., | "id": "asdf-123"
"name": ..., | "name": "Ted",
"profile: { | => "profile": {
"age": ... | "age": 18
} | }
} | }
All values the do not match the dynamic field
format will be treated as
literals during the mapping.
SPEC:
{
"literalObject": {
"literalString": "Hello, World!",
"literalInt": 42,
"literalDouble": 3.1415926535,
"literalBool": true
},
"literalArray": [],
}
The example above is essentially equivalent to output = spec
.
Dynamic values are pulled from the supplied input
; they are looked up using
json path. In order to use dynamic values, you must wrap the lookup path with
dollar-bracket syntax: ${path.to.value}
.
INPUT: SPEC: OUTPUT:
{ { {
"user": { "id": "${user.id}" "id": "asdf-123",
"id": "asdf-123", => "name": "${user.name}" => "name": "Ted"
"name": "Ted" } }
}
}
Dynamic field transformers DFT are used to modify a field. There are a number of
default DFT's included; each implements a simple interface that makes extension
simple. Values are piped (passed) to each DFT in order. They must match the
format |<key>
or if additional params are required |<key>(<optional>, <params>)
.
Input | Signature | Output |
---|---|---|
iso8601Date: String | iso8601ToMs() | String |
longStr: String | asLong() | Long |
formattedDate: String | dateToMs(format: String) | String |
boolStr: String | asBoolean() | Boolean |
INPUT:
{
"user": {
"id": "asdf-123",
"registrationTime": "2017-05-08T19:47:57+00:00",
"birthdate": "01/01/1900",
"suspended": true
}
}
SPEC:
{
"id": "${user.id|toUpperCase}",
"regDateTime": "${user.registrationTime|iso8601ToMs|asLong}",
"birthday": "${user.birthdate|dateToMs('mm/dd/yyyy')|asLong}",
"suspended": "${user.suspended|asBoolean}"
}
OUTPUT:
{
"id": "ASDF-123",
"regDateTime": 1494272877,
"birthday": 1490000000,
"suspended": true
}
Custom DFT's must implement the following interface
public interface DynamicFieldTransformer<InputT, OutputT> {
OutputT transform(InputT value, JsonNode input, List<Object> params);
}
The following is a custom DFT for concatenating multiple values together.
String | concat(String, [String]): String
Input: str: String
Signature: concat(path, [separator])
Param | Description | Required |
---|---|---|
path: String | the path to the dynamic value to be used. | true |
separator: String | the separator to use between the concat strings | false |
Output: String
public class ConcatDynamicFieldTransformer implements DynamicFieldTransformer<String, String> {
public String transform(String val, JsonNode input, List<Object> params) {
String path = params.get(0);
String separator = Optional.ofNullable(params.get(1)).orElse("");
for (String part : path.split("\\.")) {
input = input.path(part);
}
if (!input.isMissing()) {
val += separator + input.asText();
}
return val;
}
}
The following example will use a firstName
and lastName
field to create a full
name
object that includes a concatenated fullName
field:
SPEC:
{
"name": {
"firstName": "${user.first}",
"lastName": "${user.last}",
"fullName": "${user.first|concat('user.first', ' ')}"
}
}
Default values can be defined with a Spring Value injection-like syntax. For default values, an dynamic path is NOT required.
INPUT:
{}
SPEC:
{
"name": "${user.profile.name:New User}",
"status": "${:NEW}"
}
OUTPUT:
{
"name": "New User",
"status": "NEW"
}
DFT's can be applied like normal.
INPUT:
{}
SPEC:
{
"suspended": "${:false|asBoolean}"
}
OUTPUT:
{
"suspended": false
}
DFT's can ignore input (or take null input) and return dynamic values:
INPUT:
{}
SPEC:
{
"bcId": "${:|uuid}",
"regDate": "${:|newMsDate|asLong}"
}
OUTPUT:
{
"bcId": "1c9f4e90-c52f-4c5c-a6e9-4abbddd24b9c",
"regDate": 1494275446254
}