Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Polymorphic deserialization in Jackson without annotations

Tags:

I have a CloudEvent<T> class that uses polymorphic deserialization using Jackson (2.9.0 - last version) like this:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CloudEvent<T> {

    @NonNull
    private String eventType;

    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
            property = "eventType",
            defaultImpl = Void.class)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = MyEvent1.class, name = "event-1"),
            @JsonSubTypes.Type(value = MyEvent2.class, name = "event-2")
    })
    private T data;
}

And then deserialized with:

String cloudEventJson1 = "{\"eventType\":\"event-1\",\"data\":{\"id\":\"123\",\"details\":\"detail1\"}}";

CloudEvent deserializedEvent1 = objectMapper.readValue(cloudEventJson1, CloudEvent.class);   //without subtypes

All this works fine. But because some limitations, I can not use annotations on the CloudEvent class (provided by external dependency).

So I configured the ObjectMapper like this:

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.registerSubtypes(new NamedType(MyEvent1.class, "event-1"));
    objectMapper.registerSubtypes(new NamedType(MyEvent2.class, "event-2"));

    TypeResolverBuilder  typeResolverBuilder = new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE)
      .init(JsonTypeInfo.Id.NAME, null)   //CLASS works
      .inclusion(JsonTypeInfo.As.EXTERNAL_PROPERTY)
      .typeProperty("eventType")
      .typeIdVisibility(true)
//    .defaultImpl(Void.class);
    objectMapper.setDefaultTyping(typeResolverBuilder);

But deserializing with same method as above does not work. It is reading the eventType but it is not managing to match to the registered subtype. I can not use generics or TypeReferance in the deserialization because I need to use spring-integration for reading the events which only accepts main class; the pattern matching being done manually after deserialization.

Exception:

com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'event-1' as a subtype of [simple type, class java.lang.Object]: known type ids = [] (for POJO property 'data')
 at [Source: (String)"{"eventType":"event-1","data":{"id":"123","details":"detail1"}}"; line: 1, column: 271]

Also this is configuring the ObjectMapper for all input classes. Is it possible to connect this typeResolverBuilder and subtypes to the CloudEvent.class (like the annotation way does it).

like image 804
Razor Avatar asked Mar 19 '19 14:03

Razor


People also ask

What is polymorphic deserialization?

A polymorphic deserialization allows a JSON payload to be deserialized into one of the known gadget classes that are documented in SubTypeValidator. java in jackson-databind in GitHub. The deserialized object is assigned to a generic base class in your object model, such as java. lang. Object or java.

What is serialization and deserialization in Jackson?

Serialization is the process of converting the state of an object to a byte stream in a way that the byte stream can be reverted into a copy of the original object. Deserialization, as you may already have inferred, is the process of converting the serialized form of an object back into a copy of the original object.

What is ObjectMapper class in Jackson?

ObjectMapper is the main actor class of Jackson library. ObjectMapper class ObjectMapper provides functionality for reading and writing JSON, either to and from basic POJOs (Plain Old Java Objects), or to and from a general-purpose JSON Tree Model (JsonNode), as well as related functionality for performing conversions.

Does Jackson serialize getters?

Jackson by default uses the getters for serializing and setters for deserializing.


1 Answers

You can still rely on annotations even if you cannot modify your class. Jackson supports a feature called mix-ins: you can think of it as kind of aspect-oriented way of adding more annotations during runtime, to augment statically defined ones.

First define an interface as follows:

public interface CloudEventMixIn<T> {

     @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
            property = "eventType",
            defaultImpl = Void.class)
    @JsonSubTypes({
            @JsonSubTypes.Type(value = MyEvent1.class, name = "event-1"),
            @JsonSubTypes.Type(value = MyEvent2.class, name = "event-2")
    })
    public T getData();
}

Then configure ObjectMapper to use the defined interface as a mix-in for actual class/interface:

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(CloudEvent.class, CloudEventMixIn.class);

From the addMixIn(Class<?> target, Class<?> mixinSource) method documentation:

Method to use for adding mix-in annotations to use for augmenting specified class or interface. All annotations from mixinSource are taken to override annotations that target (or its supertypes) has.

like image 183
cassiomolin Avatar answered Oct 06 '22 03:10

cassiomolin