Serializing map having value of different types to JSON using Jackson in Java

Tags: , , ,



I would like to serialize a given hashmap into json and deserialize it back to the original map.

Here I would like to make this generic so that it behaves seamlessly regardless of the type of the value.

I am using the following code snippet for constructing the map and then serailizing it as json:

Map<String, Map<String, Object>> argumentNameValueMap = new HashMap<>();

for (int i = 0; i < codeSignature.getParameterNames().length; i++) {
  argumentNameValueMap.put(codeSignature.getParameterNames()[i],
      mapper.convertValue(joinPoint.getArgs()[i],
          Map.class)); <----THIS LINE IS FAILING WHEN ARGUMENT VALUE IS OF PRIMITIVE TYPE.
}

return mapper.writeValueAsString(argumentNameValueMap);

This is working fine when the type of value that is going to put into the map is an object but fails while the value is of any primitive type i.e. String/Integer etc.

I can handle this by writing a check if the associated value type is of any primitive type and add if else accordingly.

But I would like to know if there is any better way to do this. Thanks.

Answer

In JSON specification recognized values are: JSON Object{...}, JSON Array[...], string, number, false, true and null. Only JSON Object can be deserialised by default to Map and Map can be serialised to a JSON Object.

In your case you need to handle other types manually and convert them to Map instance somehow. You can implement your own com.fasterxml.jackson.databind.deser.DeserializationProblemHandler which allows to intercept conversion mechanism in case of problems.

Below you can find a simple implementation how to do that:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) {
        Map<String, Object> source = new HashMap<>();
        source.put("pojo", new SomeClass());
        source.put("string", "String-Value");
        source.put("int", 1);
        source.put("null", null);
        source.put("char", 'A');
        source.put("long", Long.MIN_VALUE);
        source.put("list", Arrays.asList(1, 3));
        source.put("array", new int[]{12, 13});

        ObjectMapper mapper = new ObjectMapper();
        mapper.addHandler(new Convert2MapDeserializationProblemHandler());

        Map<String, Map<String, Object>> argumentNameValueMap = new HashMap<>();
        for (Map.Entry<String, Object> entry : source.entrySet()) {
            argumentNameValueMap.put(entry.getKey(), mapper.convertValue(entry.getValue(), Map.class));
        }
        argumentNameValueMap.forEach((k, v) -> System.out.println(k + " -> " + v));
    }
}

class Convert2MapDeserializationProblemHandler extends DeserializationProblemHandler {
    @Override
    public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, ValueInstantiator valueInsta, JsonParser p, String msg) throws IOException {
        if (Map.class.isAssignableFrom(instClass)) {
            Map<String, Object> map = new LinkedHashMap<>();
            TreeNode value = p.readValueAsTree();
            map.put("value", value);
            return map;
        }
        return super.handleMissingInstantiator(ctxt, instClass, valueInsta, p, msg);
    }

    @Override
    public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, JsonToken t, JsonParser p, String failureMsg) throws IOException {
        if (Map.class.isAssignableFrom(targetType.getRawClass())) {
            Map<String, Object> map = new LinkedHashMap<>();
            TreeNode value = p.readValueAsTree();
            map.put("value", value);
            return map;
        }
        return super.handleUnexpectedToken(ctxt, targetType, t, p, failureMsg);
    }
}

class SomeClass {
    String stringField = "Value!";

    public String getStringField() {
        return stringField;
    }

    public void setStringField(String stringField) {
        this.stringField = stringField;
    }

    @Override
    public String toString() {
        return "SomeClass{" +
                "stringField='" + stringField + ''' +
                '}';
    }
}

Above code prints:

pojo -> {stringField=Value!}
string -> {value="String-Value"}
null -> null
array -> {value=[12,13]}
char -> {value="A"}
list -> {value=[1,3]}
int -> {value=1}
long -> {value=-9223372036854775808}


Source: stackoverflow