14

For external reasons, all java Maps in my system can only be received as lists of key-value pairs from the clients, e.g. a Map<String, Book> will actually be received as Json-serialized List<MapEntry<String, Book>>. This means I need to customize my Json deserialization process to expect this representation of maps.

The problem is that JsonDeserializer makes me implement

deserialize(JsonParser p, DeserializationContext ctxt)

method which has no access to the detected generic type it's supposed to deserialize (Map<String, Book> in the example above). Without that info, I can't in turn deserialize List<MapEntry<String, Book>> without loosing type safety.

I was looking at Converter but it gives even less context.

E.g.

public Map<K,V> convert(List<MapToListTypeAdapter.MapEntry<K,V>> list) {
    Map<K,V> x = new HashMap<>();
    list.forEach(entry -> x.put(entry.getKey(), entry.getValue()));
    return x;
}

But this will potentially create dangerous maps that will throw a ClassCastException on retrieval, as there's no way to check the type is actually sensible. Is there a way to get around this?

As an example of what I'd expect, Gson's JsonDeserializer looks like this:

T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)

I.e. it gives access to the expected type in a sane way.

kaqqao
  • 12,984
  • 10
  • 64
  • 118
  • Did you got this issue because your project is using Jackson for serialization/deserialization, and you are getting response from external system that is using Moxy? if yes, then I have the same issue. – YevgenyL Jun 18 '20 at 01:24

1 Answers1

25

Got an answer on the Jackson Google group directly from the author.

The key thing to understand is that JsonDeserializers are initiated/contextualized once, and they receive the full type and other information at that moment only. To get a hold of this info, the deserializer needs to implement ContextualDeserializer. Its createContextual method is called to initialize a deserializer instance, and has access to the BeanProperty which also gives the full JavaType.

So it could look something like this in the end:

public class MapDeserializer extends JsonDeserializer implements ContextualDeserializer {

    private JavaType type;

    public MapDeserializer() {
    }

    public MapDeserializer(JavaType type) {
        this.type = type;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) throws JsonMappingException {
        //beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property
        JavaType type = deserializationContext.getContextualType() != null 
            ? deserializationContext.getContextualType()
            : beanProperty.getMember().getType();            
        return new MapDeserializer(type);
    }

    @Override
    public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
        //use this.type as needed
    }
    
    ...
}

Registered and used as normal:

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Map.class, new MapDeserializer());
mapper.registerModule(module);
kaqqao
  • 12,984
  • 10
  • 64
  • 118