7

Consider this property

@JsonProperty
private Map<String, Object> myMap;

When a contained java.util.Date value is serialized as long, it will not be deserialized to Date again because the type information is not present in Map<String, Object>. How can I bypass the problem? I read answers about this question which would be a work around but there would be no way to distinguish strings containing dates from dates serialized as strings in the map. Can I tell Jackson to include type information for each map value such that Jackson can deserialize them correctly?

cassiomolin
  • 124,154
  • 35
  • 280
  • 359
Steffen Harbich
  • 2,639
  • 2
  • 37
  • 71
  • There is no type information in JSON. Using plain `Object` there is no way Jackson can tell dates apart from actual longs. If possible you could create a wrapper for `Object` that holds the java type information and use this to de-serialize the actual value or (if that's no option) perhaps formatting dates as ISO-8601 Strings would be possible? This way you could check the value to match an expected pattern and convert to date. – dpr Mar 21 '18 at 11:56
  • @dpr Yes, I am about to try suffixing the map key with something like `"[date]"`which will then be deserialized as date. – Steffen Harbich Mar 21 '18 at 12:13
  • Maybe this link will be useful for you. [Jackson docs](https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization) – Aliaksei Yatsau Mar 21 '18 at 21:29

2 Answers2

5

Implement a custom Deserializer and add the Annotation @JsonDeserialize(using = DateDeserializer.class) to your field.

Take a look at this example:

Your Json-Bean:

public class Foo {

    private String            name;

    @JsonProperty
    @JsonDeserialize(using = DateDeserializer.class)
    private Map<String, Object> dates;

    [...] // getter, setter, equals, hashcode
}

Deserializer:

public class DateDeserializer extends JsonDeserializer<Map<String, Object>> {

    private TypeReference<HashMap<String, Object>> typeRef = new TypeReference<HashMap<String, Object>>() {};

    @Override
    public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt, Map<String, Object> target) throws IOException, JsonProcessingException {

        Map<String, Long> map = new ObjectMapper().readValue(p, typeRef);

        for(Entry<String, Long> e : map.entrySet()){

            Long value = e.getValue();
            String key = e.getKey();

            if(value instanceof Long){ // or if("date".equals(key)) ...
                target.put(key, new Date(value));
            } else {
                target.put(key, value); // leave as is
            }

        }

        return target;
    }

    @Override
    public Map<String, Object> deserialize(JsonParser paramJsonParser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        return this.deserialize(paramJsonParser, ctxt, new HashMap<>());
    }

}

Simple test:

public static void main(String[] args) throws Exception {

    Foo foo1 = new Foo();
    foo1.setName("foo");
    foo1.setData(new HashMap<String, Object>(){{
        put("date",   new Date());
        put("bool",   true);
        put("string", "yeah");
    }});
    ObjectMapper mapper = new ObjectMapper();
    String jsonStr = mapper.writeValueAsString(foo1);
    System.out.println(jsonStr);
    Foo foo2 = mapper.readValue(jsonStr, Foo.class);

    System.out.println(foo2.equals(foo1));

}
alex
  • 8,904
  • 6
  • 49
  • 75
  • Thank you for your answer but in my case the map consists of strings, booleans, numbers and dates. Do you have an idea for that case? – Steffen Harbich Mar 20 '18 at 07:26
  • @SteffenHarbich in this case use `Map`. I have updated my answer. – alex Mar 20 '18 at 07:46
  • 1
    Ok, I think I will try to write a serializer/deserializer pair that adds/interprets some suffix or prefix to the map key depending on the type (only in case of date for now) so that this is transparent to the remaining of my code. I will give you feedback tomorrow. Thanks so far! – Steffen Harbich Mar 20 '18 at 14:21
1

Finally, I came up with this solution. Deserializer:

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

@Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt, Map<String, Object> target) throws IOException {
    Map<String, Object> map = new ObjectMapper().readValue(p, typeRef);

    for (Map.Entry<String, Object> e : map.entrySet()) {
        if (e.getKey().endsWith("[date]")) {
            target.put(e.getKey().substring(0, e.getKey().length() - 6), new Date((Long) e.getValue()));
        }
        else {
            target.put(e.getKey(), e.getValue());
        }
    }

    return target;
}

Serializer:

@Override
public void serialize(Map<String, Object> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    Map<String, Object> adaptedValue = new HashMap<>(value);

    for (Map.Entry<String, Object> e : value.entrySet()) {
        if (e.getValue() instanceof Date) {
            adaptedValue.put(e.getKey() + "[date]", ((Date) e.getValue()).getTime());
            adaptedValue.remove(e.getKey());
        }
    }

    new ObjectMapper().writeValue(gen, adaptedValue);
}

The map key is adapted dependent on the data type. This is easily extendable.

Steffen Harbich
  • 2,639
  • 2
  • 37
  • 71