Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Jackson's ContextualDeserializer for root values?

I'm trying to implement a generic deserializer for all classes that extend from a certain abstract class or implement a certain interface. In this example I'm using interface StringConvertible. I need to determine the concrete type and so that I can create an instance.

An old forum post by Programmer Bruce led me to using ContextDeserializer and it's working when the StringConvertible is a property in another class.

But when I want to deserialize the StringConvertible directly, I can't find a way to get the concrete type because the beanProperty parameter is null. Apparently that is expected, according to this question/answer at the Jackson JSON User Group: The only case where property should be null is when serializing a "root value", meaning the object instance passed directly to ObjectMapper's (or ObjectWriter's) writeValue() method -- in this case there simply isn't a referring property. But otherwise it should always be passed.

See main method below for an example of both cases:

@JsonDeserialize(using = StringConvertibleDeserializer.class)
public final class SomeStringConvertible implements StringConvertible {

    private final String value;

    public SomeStringConvertible(final String value) {

        this.value = value;
    }

    @Override
    @JsonValue
    public String stringValue() {

        return value;
    }
}

public final class SomeWrapper {

    public SomeStringConvertible stringConvertible;

    public SomeWrapper() {

    }
}


public class StringConvertibleDeserializer extends StdDeserializer<StringConvertible> implements ContextualDeserializer {

    private final Class<? extends StringConvertible>    targetClass;

    StringConvertibleDeserializer() {

        super(StringConvertible.class);

        this.targetClass = null;
    }

    StringConvertibleDeserializer(final Class<? extends StringConvertible> targetClass) {

        super(StringConvertible.class);

        this.targetClass = targetClass;
    }

    @Override
    public JsonDeserializer<?> createContextual(final DeserializationContext deserializationContext, @Nullable final BeanProperty beanProperty)
                                                                                                                                                throws JsonMappingException {

        final StringConvertibleDeserializer contextualDeserializer;

        // ====  Determine target type  =====
        final Class<? extends StringConvertible> targetClass;
        JavaType type = beanProperty.getType(); // -> beanProperty is null when the StringConvertible type is a root value
        targetClass = (Class<? extends StringConvertible>) type.getRawClass();

        // ====  Create contextual deserializer  =====
        contextualDeserializer = new StringConvertibleDeserializer(targetClass);

        // ====  Return  =====
        return contextualDeserializer;
    }

    @Override
    public StringConvertible deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {

        final StringConvertible value;

        // ====  Create instance using the target type  =====
        if (targetClass.equals(SomeStringConvertible.class))
            value = new SomeStringConvertible(jsonParser.getText());
        else {
            throw new RuntimeException();
        }

        // ====  Return  =====
        return value;
    }

}

public final class JacksonModule extends SimpleModule {

    public JacksonModule() {

        super();

        addDeserializer(StringConvertible.class, new StringConvertibleDeserializer());
    }
}

public final class Main {

    public static void main(String[] args) {

        final ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JacksonModule());

        final String wrappedValueJSON = "{\"stringConvertible\":\"hello world\"}";
        final String rootValueJSON = "\"hello world\"";

        try {
            mapper.readValue(wrappedValueJSON, SomeWrapper.class);  // This works fine
            mapper.readValue(rootValueJSON, SomeStringConvertible.class);   // This causes a NPE in createContextual(...) because beanProperty is null

        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Question: How can I get type concrete type in case of a root value? Or, if there is a better solution than this, what would you suggest instead?

like image 585
Rolf W. Avatar asked Jul 08 '15 17:07

Rolf W.


1 Answers

When researching the solution StaxMan proposed I stumbled upon this Github issue for jackson-databind which addresses the exact same problem. In response, the maintainer added a method to DeserializationContext in version 2.5.0:

This turned out relatively easy to implement, so now there is:

class DeserializationContext {
   public JavaType getContextualType() { ... }
}
which will give expected type during call to createContextual(), including case of deserializers that are directly added via annotation.

So to make this work in my case, I just had to change some code in the createContextual(...) method. I changed this:

   // ====  Determine target type  =====
    final Class<? extends StringConvertible> targetClass;
    JavaType type = beanProperty.getType(); // -> beanProperty is null when the StringConvertible type is a root value
    targetClass = (Class<? extends StringConvertible>) type.getRawClass();

To this:

// ====  Determine target type  =====
final Class<? extends StringConvertible> targetClass;
{
    // ====  Get the contextual type info  =====
    final JavaType type; 
    if (beanProperty != null) 
        type = beanProperty.getType();  // -> beanProperty is null when the StringConvertible type is a root value

    else {
        type = deserializationContext.getContextualType();
    }

    // ====  Get raw Class from type info  =====
    targetClass = (Class<? extends StringConvertible>) type.getRawClass();
}
like image 160
Rolf W. Avatar answered Sep 28 '22 04:09

Rolf W.