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).
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 thattarget
(or its supertypes) has.