27

Imagine the following scenario:

class <T> Foo<T> {
    ....
}

class Bar {
    Foo<Something> foo;
}

I want to write a custom Jackson deserializer for Foo. In order to do that (for example, in order to deserialize Bar class that has Foo<Something> property), I need to know the concrete type of Foo<T>, used in Bar, at deserialization time (e.g. I need to know that T is Something in that particluar case).

How does one write such a deserializer? It should be possible to do it, since Jackson does it with typed collections and maps.

Clarifications:

It seems there are 2 parts to solution of the problem:

1) Obtain declared type of property foo inside Bar and use that to deserialize Foo<Somehting>

2) Find out at deserialization time that we are deserializing property foo inside class Bar in order to successfully complete step 1)

How does one complete 1 and 2 ?

Krešimir Nesek
  • 5,302
  • 4
  • 29
  • 56
  • You can use reflection to find out that the *declared* type of `foo` is `Foo`. That's about it. I don't know about Jackson to say if that is a means to your end here. [`Field#getGenericType`](http://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Field.html#getGenericType--) – Radiodef Mar 22 '16 at 16:29

4 Answers4

62

You can implement a custom JsonDeserializer for your generic type which also implements ContextualDeserializer.

For example, suppose we have the following simple wrapper type that contains a generic value:

public static class Wrapper<T> {
    public T value;
}

We now want to deserialize JSON that looks like this:

{
    "name": "Alice",
    "age": 37
}

into an instance of a class that looks like this:

public static class Person {
    public Wrapper<String> name;
    public Wrapper<Integer> age;
}

Implementing ContextualDeserializer allows us to create a specific deserializer for each field in the Person class, based on the generic type parameters of the field. This allows us to deserialize the name as a string, and the age as an integer.

The complete deserializer looks like this:

public static class WrapperDeserializer extends JsonDeserializer<Wrapper<?>> implements ContextualDeserializer {
    private JavaType valueType;

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        JavaType wrapperType = property.getType();
        JavaType valueType = wrapperType.containedType(0);
        WrapperDeserializer deserializer = new WrapperDeserializer();
        deserializer.valueType = valueType;
        return deserializer;
    }

    @Override
    public Wrapper<?> deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
        Wrapper<?> wrapper = new Wrapper<>();
        wrapper.value = ctxt.readValue(parser, valueType);
        return wrapper;
    }
}

It is best to look at createContextual here first, as this will be called first by Jackson. We read the type of the field out of the BeanProperty (e.g. Wrapper<String>) and then extract the first generic type parameter (e.g. String). We then create a new deserializer and store the inner type as the valueType.

Once deserialize is called on this newly created deserializer, we can simply ask Jackson to deserialize the value as the inner type rather than as the whole wrapper type, and return a new Wrapper containing the deserialized value.

In order to register this custom deserializer, we then need to create a module that contains it, and register that module:

SimpleModule module = new SimpleModule()
        .addDeserializer(Wrapper.class, new WrapperDeserializer());

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);

If we then try to deserialize the example JSON from above, we can see that it works as expected:

Person person = objectMapper.readValue(json, Person.class);
System.out.println(person.name.value);  // prints Alice
System.out.println(person.age.value);   // prints 37

There are some more details about how contextual deserializers work in the Jackson documentation.

jmattheis
  • 10,494
  • 11
  • 46
  • 58
andersschuller
  • 13,509
  • 2
  • 42
  • 33
  • 2
    Thank you for this post. Incredibly helpful to achieve what I wanted. I have made a few alterations that works with root values in case anyone else from Google comes around. https://gist.github.com/darylteo/a7be65b539c0d8d3ca0de94d96763f33 – Daryl Teo Sep 07 '16 at 07:30
  • 2
    Thank you, that really helped The contextual type itself could be generic, in that case property will be null, you need to create the JsonDeserializer using the ctxt.getContextualType – Ahmed Mkaouar Aug 10 '17 at 09:30
  • What happens when value inside Wrapper is private, and is set by a constructor with arguments? – Candy Chiu Mar 23 '18 at 21:55
  • If I have a list of people public class People { public List people; } how can I do? – Pako Apr 22 '18 at 09:10
  • Thank you, this really helped in writing a custom deserialiser for Quantity and Quantity types. Helped in figuring out which type in use. – Smruti R Tripathy Nov 17 '20 at 12:51
  • I am having an issue if the Generic is inside a list. I am getting NPE and the readTree looks better. Any chance to include this at the answer ? – Jimmy Kane Feb 23 '21 at 13:40
  • Is this solution thread safe...? We instantiate WrapperDeserializer & add it to the module, if multiple requests came in parallel isn't there the risk that we could refer to the wrong JavaType ? – Kevvvvyp Feb 25 '21 at 18:05
7

If the target itself is a generic type then property will be null, for that you'll need to get the valueTtype from the DeserializationContext:

@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
    if (property == null) { //  context is generic
        JMapToListParser parser = new JMapToListParser();
        parser.valueType = ctxt.getContextualType().containedType(0);
        return parser;
    } else {  //  property is generic
        JavaType wrapperType = property.getType();
        JavaType valueType = wrapperType.containedType(0);
        JMapToListParser parser = new JMapToListParser();
        parser.valueType = valueType;
        return parser;
    }
}
Ahmed Mkaouar
  • 638
  • 6
  • 4
  • 1
    For anyone getting an NPE on the accepted answer, this solution is probably what you need. Thankyou - I have no idea how much time you just saved me. – Martin Jan 14 '18 at 11:14
  • 2
    Definitely extremely helpful. I was actually using this in Kotlin, and there are a few caveats. For example, generic type information can be passed incompletely depending on how you read the value: `mapper.readValue>` preserves the inner type T, while the Java method `mapper.readValue(json, Generic::class.java)` does not. Somehow obvious, yet caused some NPEs. This exercise taught me quite a bit about generics in JVM. – TheOperator Nov 27 '18 at 22:27
  • @TheOperator do you have an example please ? – Jimmy Kane Feb 23 '21 at 13:37
3

This is how you can access/resolve {targetClass} for a Custom Jackson Deserializer. Of course you need to implement ContextualDeserializer interface for this.

public class WPCustomEntityDeserializer extends JsonDeserializer<Object> 
              implements ContextualDeserializer {

    private Class<?> targetClass;

    @Override
    public Object deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {

        ObjectCodec oc = jp.getCodec();
        JsonNode node = oc.readTree(jp);

        //Your code here to customize deserialization
        // You can access {target class} as targetClass (defined class field here)
        //This should build some {deserializedClasObject}

        return deserializedClasObject;

    }   

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property){
        //Find here the targetClass to be deserialized  
        String targetClassName=ctxt.getContextualType().toCanonical();
        try {
            targetClass = Class.forName(targetClassName);
        } catch (ClassNotFoundException e) {            
            e.printStackTrace();
        }
        return this;
    }
}
Pratap Singh
  • 401
  • 1
  • 4
  • 14
0

For my use case, none of the above solutions worked, so I had to write a custom module. You can find my implementation on GitHub.

I wanted to write a deserializer that automatically removes blank Strings from Lists.

Alwin
  • 786
  • 9
  • 19