bazaarvoice / jolt

JSON to JSON transformation library written in Java.
Apache License 2.0
1.56k stars 329 forks source link

Use JOLT to “stringify” JSON #1079

Closed Karric closed 2 years ago

Karric commented 3 years ago

I have a situation where my only method for modifying an API response's data is JOLT. For various reasons I need to 'stringify' the response using JOLT.

It's easy enough to use =concat to do this but it doesn't produce a valid JSON format, or something that's easily reversed.

Example Input

{"random":69,"random float":41.842,"bool":false,"date":"1988-01-23","array":["Pearline","Marita","Sonni"],"array of objects":[{"index":0,"index start at 5":5}],"Ernesta":{"age":73}}

JOLT

[
  {
    "operation": "shift",
    "spec": {
      "*": {
        "@": "x.&"
      }
    }
    },
  {
    "operation": "modify-overwrite-beta",
    "spec": {
      "*": "=concat('',@(0))"
    }
    },
  {
    "operation": "shift",
    "spec": {
      "x": ""
    }
    }
]

OUTPUT

"{random=69, random float=41.842, bool=false, date=1988-01-23, array=[Pearline, Marita, Sonni], array of objects=[{index=0, index start at 5=5}], Ernesta={age=73}}"

DESIRED OUTPUT

"{\"random\":69,\"random float\":41.842,\"bool\":false,\"date\":\"1988-01-23\",\"array\":[\"Pearline\",\"Marita\",\"Sonni\"],\"array of objects\":[{\"index\":0,\"index start at 5\":5}],\"Ernesta\":{\"age\":73}}"

I tried to check the JOLT java code to see if I could tweak concat's behaviour to not lose the quotation marks, but I could not find anything.

I thought about using concat to restore the double quotes and colon but I quickly gave up. If anyone has a better technique, please let me know. Ideally the JOLT is generalized and not input-specific.

lucioalmeida commented 3 years ago

Its possible with custom modifier

package br.com.teste.jolt;

import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.modifier.function.Function;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CustomFunctions {

    public static final class stringfy extends Function.SingleFunction<Object> {

        @Override
        protected Optional<Object> applySingle(Object o) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                String stringValue = objectMapper.writeValueAsString(o);
                return Optional.of(stringValue);
            } catch (JsonProcessingException e) {
                return Optional.empty();
            }
        }
    }
}
package br.com.teste.jolt;

import com.bazaarvoice.jolt.ContextualTransform;
import com.bazaarvoice.jolt.SpecDriven;
import com.bazaarvoice.jolt.common.Optional;
import com.bazaarvoice.jolt.common.tree.MatchedElement;
import com.bazaarvoice.jolt.common.tree.WalkedPath;
import com.bazaarvoice.jolt.exception.SpecException;
import com.bazaarvoice.jolt.modifier.OpMode;
import com.bazaarvoice.jolt.modifier.TemplatrSpecBuilder;
import com.bazaarvoice.jolt.modifier.function.Function;
import com.bazaarvoice.jolt.modifier.spec.ModifierCompositeSpec;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class CustomModifier implements SpecDriven, ContextualTransform {

    private static final Map<String, Function> STOCK_FUNCTIONS = new HashMap<>();

    private final ModifierCompositeSpec rootSpec;

    static {
        STOCK_FUNCTIONS.put("stringfy", new CustomFunctions.stringfy());
    }

    @SuppressWarnings("unchecked")
    private CustomModifier(Object spec, OpMode opMode, Map<String, Function> functionsMap) {
        if (spec == null) {
            throw new SpecException(opMode.name() + " expected a spec of Map type, got 'null'.");
        }

        if (!(spec instanceof Map)) {
            throw new SpecException(opMode.name() + " expected a spec of Map type, got " + spec.getClass().getSimpleName());
        }

        if (functionsMap == null || functionsMap.isEmpty()) {
            throw new SpecException(opMode.name() + " expected a populated functions' map type, got " + (functionsMap == null ? "null" : "empty"));
        }

        functionsMap = Collections.unmodifiableMap(functionsMap);
        TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder(opMode, functionsMap);

        rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map<String, Object>) spec, opMode, templatrSpecBuilder);
    }

    @Override
    public Object transform(final Object input, final Map<String, Object> context) {

        Map<String, Object> contextWrapper = new HashMap<>();
        contextWrapper.put(ROOT_KEY, context);

        MatchedElement rootLpe = new MatchedElement(ROOT_KEY);
        WalkedPath walkedPath = new WalkedPath();
        walkedPath.add(input, rootLpe);

        rootSpec.apply(ROOT_KEY, Optional.of(input), walkedPath, null, contextWrapper);

        return input;
    }

    /**
     * This variant of modifier creates the key/index is missing,
     * and overwrites the value if present
     */
    public static final class Overwritr extends CustomModifier {

        public Overwritr(Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Overwritr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.OVERWRITR, functionsMap);
        }
    }

    /**
     * This variant of modifier only writes when the key/index is missing
     */
    public static final class Definr extends CustomModifier {

        public Definr(final Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Definr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.DEFINER, functionsMap);
        }
    }

    /**
     * This variant of modifier only writes when the key/index is missing or the value is null
     */
    public static class Defaultr extends CustomModifier {

        public Defaultr(final Object spec) {
            this(spec, STOCK_FUNCTIONS);
        }

        public Defaultr(Object spec, Map<String, Function> functionsMap) {
            super(spec, OpMode.DEFAULTR, functionsMap);
        }
    }

}

Spec:

[
  {
    "operation": "shift",
    "spec": {
      "*": {
        "@": "x.&"
      }
    }
    },
  {
    "operation": "br.com.teste.jolt.CustomModifier$Overwritr",
    "spec": {
      "*": "=stringfy(@(0))"
    }
    },
  {
    "operation": "shift",
    "spec": {
      "x": ""
    }
    }
]

output "{\"random\":69,\"random float\":41.842,\"bool\":false,\"date\":\"1988-01-23\",\"array\":[\"Pearline\",\"Marita\",\"Sonni\"],\"array of objects\":[{\"index\":0,\"index start at 5\":5}],\"Ernesta\":{\"age\":73}}"