Unchecked cast on a Map<String, Object> (JSON converted to Map with Jackson)

norbjd :

In Java 8, I want to convert a JSON string to a map, and apply "complex" transformations to the keys. As an example, that "complex" transformation will simply be a lower case transformation. Values in the input JSON could be strings or nested JSON objects. My code is actually working, but I struggle to fix an unchecked cast warning.

Example

Example JSON input (String) :

{
    "Key1": "value1",
    "Key2": {
        "Key2.1": "value2.1"
    }
}

Desired output (Map) :

"key1" -> "value1"
"key2" ->
    "key2.1" -> "value2.1"

1. JSON String -> Map

For that part, I used Jackson (2.9.8) and defined the following function :

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

Map<String, Object> convertJsonStringToMap(String json) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    TypeReference type = new TypeReference<Map<String, Object>>(){};
    return mapper.readValue(json, type);
}

Since values can be strings or JSON objects, the return type of that function is Map<String, Object>. Note also that readValue (in ObjectMapper class) uses generics, its signature is :

<T> T readValue(String content, TypeReference valueTypeRef)

2. Map -> Map with transformed keys (example : lower case)

I defined the following function :

Map<String, Object> transformKeys(Map<String, Object> map) {
    Map<String, Object> result = new HashMap<>(map.size());

    for (Map.Entry<String, Object> entry : map.entrySet()) {
        Object value = entry.getValue();
        if (value instanceof Map) {
            value = transformKeys((Map<String, Object>) value);
        }
        // toLowerCase() is the transformation here (example), but I could have used something else
        result.put(entry.getKey().toLowerCase(), value);
    }

    return result;
}

To handle nested maps, this function is recursive. But since it takes a Map<String, Object> as parameter, I must cast value to Map<String, Object> to call the method recursively.

3. Putting all together

String json = "{\"Key1\": \"value1\", \"Key2\": { \"Key2.1\": \"value2.1\" }}";
Map<String, Object> initialMap = convertJsonStringToMap(json);
Map transformedMap = transformKeys(initialMap);
System.out.println(transformedMap);

This code works, and prints, as expected :

{key1=value1, key2={key2.1=value2.1}}

But this line in transformKeys function :

value = transformKeys((Map<String, Object>) value);

produces a warning :

[WARNING] App.java:[29,74] unchecked cast
    required: java.util.Map<java.lang.String,java.lang.Object>
    found:    java.lang.Object

The warning is clear and I understand it (the compiler cannot know if value is really an instance of Map<String, Object>), but is there a way to get rid of it?

(No @SuppressWarnings please, nor -Xlint:none) :D

I feel like returning a Map<String, Object> from convertJsonStringToMap is not the cleanest way to convert JSON String to a Map, but I can't find an other way to do it with Jackson.

Michał Ziober :

You need to use Object as Map value because it could be another Map, List or primitive (String, Integer, etc.). Jackson allows also to manipulate JSON using JsonNode types. We need to traverse JSON object but also JSON array (you forgot about it). In that case we need to:

  • Deserialise JSON to JsonNode.
  • Traverse it using JsonNode API.
  • Convert to the Map

Simple implementation:

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        String json = "{\"Key1\": \"value1\", \"Key2\": { \"Key2.1\": \"value2.1\" }, \"Key3\":[{\"pRiMe\":11}]}";
        Map<String, Object> map = convertJsonStringToMap(json);

        System.out.println(map);
    }

    private static Map<String, Object> convertJsonStringToMap(String json) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode node = mapper.readTree(json);
        transformKeys(node);

        TypeReference mapType = new TypeReference<Map<String, Object>>() {
        };

        return mapper.convertValue(node, mapType);
    }

    private static void transformKeys(JsonNode parent) {
        if (parent.isObject()) {
            ObjectNode node = (ObjectNode) parent;

            List<String> names = new ArrayList<>();
            node.fieldNames().forEachRemaining(names::add);

            names.forEach(name -> {
                JsonNode item = node.remove(name);
                transformKeys(item);
                node.replace(name.toLowerCase(), item);
            });
        } else if (parent.isArray()) {
            ArrayNode array = (ArrayNode) parent;
            array.elements().forEachRemaining(JsonApp::transformKeys);
        }
    }
}

Above code prints:

{key1=value1, key2={key2.1=value2.1}, key3=[{prime=11}]}

We got rid of unsafe casting and our implementation is more concise.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=158950&siteId=1