Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get the detected generic type inside Jackson's JsonDeserializer

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.

like image 923
kaqqao Avatar asked Nov 17 '17 10:11

kaqqao


1 Answers

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

The key thing to understand is that JsonDeserializers are created/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);
like image 177
kaqqao Avatar answered Sep 21 '22 17:09

kaqqao