Skip to content

Instantly share code, notes, and snippets.

@david-bc
Last active May 8, 2017 21:03
Show Gist options
  • Save david-bc/48305344218810594ad2383ac749d10a to your computer and use it in GitHub Desktop.
Save david-bc/48305344218810594ad2383ac749d10a to your computer and use it in GitHub Desktop.
j2j4J: JSON to JSON for Java Transformation Proposal

Intro

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)

Output Structure

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
  }               |          }
}                 |       }

Hardcoded Values

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

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 Transformer

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
}

Interface

Custom DFT's must implement the following interface

public interface DynamicFieldTransformer<InputT, OutputT> {

  OutputT transform(InputT value, JsonNode input, List<Object> params);
}
Example: String Concat

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

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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment