Aleksandar Dimitrov
Aleksandar Dimitrov

Reputation: 9487

Deserialize Generic Type using ReferenceTypeDeserializer with Jackson & Spring

I think I'm missing something obvious here, but I can't seem to be able to deserialise a simple generic container using Spring/Kotlin/Jackson.

The data type in question is very simple:

@JsonDeserialize(using = PatchableDeserializer::class)
sealed class Patchable<T> {
    class Undefined<T>: Patchable<T>()
    class Null<T>: Patchable<T>()
    data class Present<T>(val content: T): Patchable<T>()
    // …
}

The deserializer extends ReferenceTypeDeserializer, just as the jdk8-module's OptionalDeserializer.

class PatchableDeserializer(javaType: JavaType, vi: ValueInstantiator, typeDeser: TypeDeserializer, deser: JsonDeserializer<*> ):
        ReferenceTypeDeserializer<Patchable<*>>(javaType, vi, typeDeser, deser) {
    // …
}

I assumed that Jackson would fill in the constructor arguments for PatchableDeserializer here. However, that does not seem to be the case:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'my.namespace.PatchableDeserializer': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.fasterxml.jackson.databind.JavaType' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

I would assume that Jackson provides the value of javaType as I have no way of knowing it at compile time.

Here's the code I'm using to test, which generates the above exception:

@RunWith(SpringRunner::class)
@JsonTest
class PatchableTest {
    @Autowired
    lateinit var objectMapper: ObjectMapper

    @Test
    fun patchableDeserialisesStringValue() {
        val value: Patchable<String> = objectMapper.readValue("\"null\"", object: TypeReference<Patchable<String>>() {})
        assertTrue(value.isPresent())
        assertEquals("null", value.unsafeGetValue())
    }
}

What am I missing? Also, I had a really hard time looking online for some information on how to deserialize generic types at all, so if anybody has pointers to how to write custom deserialisers for generic types, I'd be very appreciative.

Upvotes: 3

Views: 1537

Answers (2)

Ruslan Stelmachenko
Ruslan Stelmachenko

Reputation: 5439

Deserializes of generic containers (those extends com.fasterxml.jackson.databind.deser.std.ReferenceTypeDeserializer) cannot be registered this way.

You need to register a custom implementation of com.fasterxml.jackson.databind.deser.Deserializers with overriding findReferenceDeserializer method. This method called to locate deserializer for value that is of referential type.

You also need to add com.fasterxml.jackson.databind.type.TypeModifier that will modify the type of your "generic container" type to be com.fasterxml.jackson.databind.type.ReferenceType, so your custom ReferenceTypeDeserializer will be called.

The easiest way to do this is to register a custom com.fasterxml.jackson.databind.Module that will addDeserializers and addTypeModifier. A good example of such module is Jdk8Module that does all the work for java.util.Optional and it's friends.

The full example to register deserializer for a custom generic container MyRef:

MyRefDeserializer that does main work. This is just an example. The exact implementation of overridden methods (and what methods need to be overridden) will depend on your requirements.

public final class MyRefDeserializer extends ReferenceTypeDeserializer<MyRef<?>> {

    public MyRefDeserializer(JavaType fullType, ValueInstantiator vi,
            TypeDeserializer typeDeser, JsonDeserializer<?> deser) {
        super(fullType, vi, typeDeser, deser);
    }

    @Override
    protected MyRefDeserializer withResolved(TypeDeserializer typeDeser, JsonDeserializer<?> valueDeser) {
        return new MyRefDeserializer(_fullType, _valueInstantiator, typeDeser, valueDeser);
    }

    @Override
    public MyRef<?> getNullValue(DeserializationContext ctxt) throws JsonMappingException {
        return MyRef.of(_valueDeserializer.getNullValue(ctxt));
    }

    @Override
    public Object getAbsentValue(DeserializationContext ctxt) throws JsonMappingException {
        return MyRef.absent();
    }

    @Override
    public MyRef<?> referenceValue(Object contents) {
        return MyRef.of(contents);
    }

    @Override
    public Object getReferenced(MyRef<?> reference) {
        return reference.orElse(null);
    }

    @Override
    public NullableOptional<?> updateReference(MyRef<?> reference, Object contents) {
        return referenceValue(contents);
    }

}

Implementation of Deserializers interface:

public class MyRefDeserializers extends Deserializers.Base {

    @Override
    public JsonDeserializer<?> findReferenceDeserializer(ReferenceType refType, DeserializationConfig config,
            BeanDescription beanDesc, TypeDeserializer contentTypeDeserializer, JsonDeserializer<?> contentDeserializer)
            throws JsonMappingException {

        if (refType.hasRawClass(MyRef.class)) {
            return new MyRefDeserializer(refType, null, contentTypeDeserializer, contentDeserializer);
        }

        return null;
    }

}

An implementation of TypeModifier that allows Jackson to understand that MyRef is a ReferenceType type:

public class MyRefTypeModifier extends TypeModifier {

    @Override
    public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, TypeFactory typeFactory) {
        if (type.isReferenceType() || type.isContainerType()) {
            return type;
        }

        if (type.getRawClass() == MyRef.class) {
            return ReferenceType.upgradeFrom(type, type.containedTypeOrUnknown(0));
        } else {
            return type;
        }

    }

}

And, finally, the Module that will register the deserializer and the type modifier:

public class MyRefModule extends Module {

    @Override
    public String getModuleName() {
        return "MyRefModule";
    }

    @Override
    public Version version() {
        return Version.unknownVersion();
    }

    @Override
    public void setupModule(SetupContext context) {
        context.addDeserializers(new MyRefDeserializers());
        context.addTypeModifier(new MyRefTypeModifier());
    }

}

You then need to register this module with ObjectMapper. E.g.:

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

If you use Spring Boot, you can just expose the module as a @Bean and it will be registered in the default ObjectMapper automatically.

Upvotes: 1

Aleksandar Dimitrov
Aleksandar Dimitrov

Reputation: 9487

I ended up implementing a different interface for my deserialiser.

class PatchableDeserializer(private val valueType: Class<*>?): JsonDeserializer<Patchable<*>>(), ContextualDeserializer {
    override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
        val wrapperType = property?.type

        val rawClass = wrapperType?.containedType(0)?.rawClass
        return PatchableDeserializer(rawClass)
    }

    override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Patchable<*> =
        Patchable.of(p!!.readValueAs(valueType))

    override fun getNullValue(ctxt: DeserializationContext?): Patchable<Any> =
            if (ctxt?.parser?.currentToken == JsonToken.VALUE_NULL)
                Patchable.ofNull()
            else
                Patchable.undefined()
}

This works as intended, but Jackson needs contextual information in the parser to make it work, i.e. the above test code does not work. It does work however, if you explicitly specify a DTO to deserialise to, if it has the correct type annotations.

Upvotes: 0

Related Questions