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 )
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
.
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;
}
}