How to modify the value of a JsonNode recursively

2019-08-10 01:26发布

问题:

Requirements:
I want to apply some functions on the inner values of the JsonNode. The functions can be different eg:- lowercasing some values or appending something to the values or replace the values with something. How can I achieve that using Jackson library? Note that the structure of the JSON data can be different which means I want to build a generic system which will accept some path expression which will basically decide where to change. I want to use functional programming style, so that I can pass these functions as arguments.

eg:

input:

{
  "name": "xyz",
  "values": [
    {
      "id": "xyz1",
      "sal": "1234",
      "addresses": [
        {
          "id": "add1",
          "name": "ABCD",
          "dist": "123"
        },
        {
          "id": "add2",
          "name": "abcd3",
          "dist": "345"
        }
      ]
    },
    {
      "id": "xyz2",
      "sal": "3456",
      "addresses": [
        {
          "id": "add1",
          "name": "abcd",
          "dist": "123"
        },
        {
          "id": "add2",
          "name": "XXXXX",
          "dist": "345"
        }
      ]
    }
  ]
}

In this case I have to two functions basically, lowercase() and convert_to_number(). I want to apply lowercase() function on all the "name" attribute inside all the "addresses" of each "value". same goes for convert_to_number() , but for all the "dist" attribute.

So, basically the JSON expressions will be something like below for the functions:

lowercase() : /values/*/addresses/*/name
convert_to_number() : /values/*/addresses/*/dist

output:

{
  "name": "xyz",
  "values": [
    {
      "id": "xyz1",
      "sal": "1234",
      "addresses": [
        {
          "id": "add1",
          "name": "abcd",
          "dist": 123
        },
        {
          "id": "add2",
          "name": "abcd3",
          "dist": 345
        }
      ]
    },
    {
      "id": "xyz2",
      "sal": "3456",
      "addresses": [
        {
          "id": "add1",
          "name": "abcd",
          "dist": 123
        },
        {
          "id": "add2",
          "name": "xxxx",
          "dist": 345
        }
      ]
    }
  ]
}

Client code:

JsonNode jsonNode = ...
applyFunctionsRecursivelyBasedOnExpr(JsonNode jsonNode, String expr, Function )

回答1:

JsonPath

You could use JsonPath library which has a better JSON Path handling. When Jackson supports only JSON Pointer draft-ietf-appsawg-json-pointer-03. Take a look on JsonPointer documentation. With JsonPath library you could do that in this way:

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

public class JsonPathApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        JsonModifier jsonModifier = new JsonModifier(jsonFile);
        Function<Map<String, Object>, Void> lowerCaseName = map -> {
            final String key = "name";
            map.put(key, map.get(key).toString().toLowerCase());
            return null;
        };
        Function<Map<String, Object>, Void> changeDistToNumber = map -> {
            final String key = "dist";
            map.put(key, Integer.parseInt(map.get(key).toString()));
            return null;
        };
        jsonModifier.update("$.values[*].addresses[*]", Arrays.asList(lowerCaseName, changeDistToNumber));
        jsonModifier.print();
    }
}

class JsonModifier {

    private final DocumentContext document;

    public JsonModifier(File json) throws IOException {
        this.document = JsonPath.parse(json);
    }

    public void update(String path, List<Function<Map<String, Object>, Void>> transformers) {
        JSONArray array = document.read(path);
        for (int i = 0; i < array.size(); i++) {
            Object o = array.get(i);
            transformers.forEach(t -> {
                t.apply((Map<String, Object>) o);
            });
        }
    }

    public void print() {
        System.out.println(document.jsonString());
    }
}

Your path, should work on JSON object-s which are represented by Map<String, Object>. You can replace keys in given object, add them, remove them just like replacing, adding and removing keys in Map.

Jackson

You can of course mock JsonPath feature by iterating over Json Pointer. For each * we need to create loop and iterate over it using counter and until node is not missing. Below you can see simple implementation:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        JsonNode root = mapper.readTree(jsonFile);

        Function<ObjectNode, Void> lowerCaseName = node -> {
            final String key = "name";
            node.put(key, node.get(key).asText().toLowerCase());
            return null;
        };
        Function<ObjectNode, Void> changeDistToNumber = node -> {
            final String key = "dist";
            node.put(key, Integer.parseInt(node.get(key).asText()));
            return null;
        };

        JsonModifier jsonModifier = new JsonModifier(root);
        jsonModifier.updateAddresses(Arrays.asList(lowerCaseName, changeDistToNumber));

        System.out.println(mapper.writeValueAsString(root));
    }
}

class JsonModifier {

    private final JsonNode root;

    public JsonModifier(JsonNode root) {
        this.root = root;
    }

    public void updateAddresses(List<Function<ObjectNode, Void>> transformers) {
        String path = "/values/%d/addresses/%d";
        for (int v = 0; v < 100; v++) {
            int a = 0;
            do {
                JsonNode address = root.at(String.format(path, v, a++));
                if (address.isMissingNode()) {
                    break;
                }
                if (address.isObject()) {
                    transformers.forEach(t -> t.apply((ObjectNode) address));
                }
            } while (true);
            if (a == 0) {
                break;
            }
        }
    }
}

This solution is slower than with JsonPath because we need to travers whole JSON tree n times where n number of matching nodes. Of course, our implementation could be a much faster using Streaming API.



回答2:

As @MichalZiober in his answer already pointed out, JsonPath offers a much more powerful API than Jackson, when you need to do JSON-path-based operations.

Using methods JsonPath.parse and DocumentContext.map you can solve your problem in just a few lines:

import java.io.File;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;

public class Main {

    public static void main(String[] args) throws Exception {
        File file = new File("input.json");
        DocumentContext context = JsonPath.parse(file);
        context.map("$.values[*].addresses[*].name", Main::lowerCase);
        context.map("$.values[*].addresses[*].dist", Main::convertToNumber);
        String json = context.jsonString();
        System.out.println(json);
    }

    private static Object lowerCase(Object currentValue, Configuration configuration) {
        if (currentValue instanceof String)
            return ((String)currentValue).toLowerCase();
        return currentValue;
    }

    private static Object convertToNumber(Object currentValue, Configuration configuration) {
        if (currentValue instanceof String)
            return Integer.valueOf((String)currentValue);
        return currentValue;
    }
}