Jackson Polymorphism: How to Map Multiple Subtypes to the Same Class

Jackson polymorphism: How to map multiple subtypes to the same class

Perhaps not by using annotations. Problems comes from the fact that such mapping would not work for serialization, and existing mapping does expect one-to-one (bijection) relationship.
But you may want to file an RFE at jackson-databind issue tracker; adding support may be possible.

Deserialize multi level polymorphic subtypes using jackson annotations

Multiple levels of polymorphic type hierarchy is not supported by Jackson api’s.

You can have a look at : https://github.com/FasterXML/jackson-databind/issues/374

So what you need to do is:

Please create a deserializer (for MySubItem.class say MySubItemDeserializerMixin.class) for and configure that to jsonMapper as we do for other Mixin classes.

mapper.addMixInAnnotations(MySubItem.class, MySubItemDeserializerMixin.class);

The MySubItemDeserializerMixin.java would look like :

@JsonDeserialize(using = MySubItemDeserializer.class)
public abstract class MySubItemDeserializerMixin{
}

You would also need to create a deserializer(MySubItemDeserializer) for MySubItem as specified in MySubItemDeserializerMixin.java.

Now you need to create MySubItemMixin.java which would look like this:

@JsonTypeInfo(use=Id.MINIMAL_CLASS, include=As.PROPERTY, property="type")
@JsonSubTypes({
@Type(MySubItemA.class)
@Type(MySubItemB.class)
@Type(MySubItemC.class)
})

In MySubItemDeserializer you would do something like:

@Override
public MySubItem deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {

JsonNode node = jsonParser.getCodec().readTree(jsonParser);

ObjectMapper jsonMapper = new ObjectMapper();

// Omit null values from the JSON.
jsonMapper.setSerializationInclusion(Include.NON_NULL);

// Treat received empty JSON strings as null Java values.
// Note: doesn't seem to work - using custom deserializer through module
// below instead.
jsonMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);

jsonMapper.addMixInAnnotations(MySubItem.class, MySubItemMixin.class);

// Enable interpretation of JAXB annotations on our beans (e.g. for
// resolving ID/IDREF references).
jsonMapper.registerModule(new JaxbAnnotationModule());

MySubItem condition = jsonMapper.readValue(node.toString(), MySubItem.class);

}

Hope that resolves your concerns.

Thanks & Regards

Nakul Vashishth

Deserializing polymorphic types with Jackson based on the presence of a unique property

This feels like something @JsonTypeInfo and @JsonSubTypes should be used for but I've picked through the docs and none of the properties that can be supplied quite seem to match what you're describing.

You could write a custom deserializer that uses @JsonSubTypes' "name" and "value" properties in a non-standard way to accomplish what you want. The deserializer and @JsonSubTypes would be supplied on your base class and the deserializer would use the "name" values to check for the presence of a property and if it exists, then deserialize the JSON into the class supplied in the "value" property. Your classes would then look something like this:

@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
@Type(name = "stringA", value = SubClassA.class),
@Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
private Long id;
...
}

public class SubClassA extends Parent {
private String stringA;
private Integer intA;
...
}

public class SubClassB extends Parent {
private String stringB;
private Integer intB;
...
}

Specifying Jackson JSON subtypes on something other than the base class due to circular dependency

You may skip @JsonSubTypes and listing subtypes by name, if you are happy putting (part of) the class name in JSON instead of a custom name you specify. E.g.

@JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY)
abstract class Animal {

Will result in a serialisation of Pets like (note the dot before class name):

{"myPets":[{"@c":".Dog","name":"n"},{"@c":".Cat","name":"n"}]}

or with use = JsonTypeInfo.Id.CLASS you also get the full package:

{"myPets":[

{"@class":"com.example.Dog","name":"n"},

{"@class":"com.example.Cat","name":"n"}

]}

Check docs for JsonTypeInfo

Alternatively, if all subclasses of Animal are in the same package (different than Animal) and you define ObjectMapper in that, you can use mixins to specify subtypes and avoid "circular dependency":

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Dog.class, name = "Dog"),
@JsonSubTypes.Type(value = Cat.class, name = "Cat") }
)
interface AnimalMixIn {}

and

mapper.addMixIn(Animal.class, AnimalMixIn.class);

in this case there is no @JsonTypeInfo, @JsonSubTypes on Animal

Is Jackson's @JsonSubTypes still necessary for polymorphic deserialization?

There are two ways to achieve polymorphism in serialization and deserialization with Jackson. They are defined in Section 1. Usage in the link you posted.

Your code

@JsonTypeInfo(
use = JsonTypeInfo.Id.MINIMAL_CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "@class")

is an example of the second approach. The first thing to note is that

All instances of annotated type and its subtypes use these settings
(unless overridden by another annotation)

So this config value propagates to all subtypes. Then, we need a type identifier that will map a Java type to a text value in the JSON string and vice versa. In your example, this is given by JsonTypeInfo.Id#MINIMAL_CLASS

Means that Java class name with minimal path is used as the type identifier.

So a minimal class name is generated from the target instance and written to the JSON content when serializing. Or a minimal class name is used to determine the target type for deserializing.

You could have also used JsonTypeInfo.Id#NAME which

Means that logical type name is used as type information; name will
then need to be separately resolved to actual concrete type (Class).

To provide such a logical type name, you use @JsonSubTypes

Annotation used with JsonTypeInfo to indicate sub types of
serializable polymorphic types, and to associate logical names used
within JSON content (which is more portable than using physical Java
class names).

This is just another way to achieve the same result. The documentation you're asking about states

Type ids that are based on Java class name are fairly
straight-forward: it's just class name, possibly some simple prefix
removal (for "minimal" variant). But type name is different: one has
to have mapping between logical name and actual class.

So the various JsonTypeInfo.Id values that deal with class names are straight-forward because they can be auto-generated. For type names, however, you need to give the mapping value explicitly.

Mapping JSON to polymorphic POJOs with single- and multi-properties

You have used an abstract class on the FieldValue level to use it in FIeld class. In that case you can construct the object with type=email and value=address which can lead to some issues...

I would recommend to create a specific classes for every type with specific FieldValue type.
The following code is serializing/deserializing JSON from/to required format from/to POJO:

public class Main {
String json = "{\"id\":1,\"fields\":[{\"type\":\"SIMPLE\",\"value\":\"Simple Value\"},{\"type\":\"NAME\",\"value\":{\"firstName\":\"first name\",\"lastName\":\"last name\"}}]}";

public static void main(String []args) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();

String json = objectMapper.writeValueAsString(generate());
System.out.println(json);

System.out.println(objectMapper.readValue(json, Contact.class));
}

private static Contact generate() {
SimpleField simpleField = SimpleField.builder().type(FieldType.SIMPLE).value("Simple Value").build();

NameFieldValue nameFieldValue = NameFieldValue.builder().firstName("first name").lastName("last name").build();
NameField nameField = NameField.builder().type(FieldType.NAME).value(nameFieldValue).build();

return Contact.builder().id(1).fields(Arrays.asList(simpleField, nameField)).build();
}
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = SimpleField.class, name = "SIMPLE"),
@JsonSubTypes.Type(value = NameField.class, name = "NAME")
})
interface Field {
FieldType getType();
Object getValue();
}

enum FieldType {
SIMPLE, NAME
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class Contact {
private int id;
private List<Field> fields;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class SimpleField implements Field {
private FieldType type;
private String value;

@Override
public FieldType getType() {
return this.type;
}

@Override
public String getValue() {
return this.value;
}
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class NameField implements Field {
private FieldType type;
private NameFieldValue value;

@Override
public FieldType getType() {
return this.type;
}

@Override
public Object getValue() {
return this.value;
}
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class NameFieldValue {
private String firstName;
private String lastName;
}

I have used lombok library here just to minimize the code and avoiding creating getters/setters as well as constructors. You can delete lombok annotations and add getters/setters/constructors and code will work the same.

So, the idea is that you have a Contact class (which is root of your JSON) with a list of Fields (where Field is an interface). Every Field type has own implementation like NameField implements Field and has NameFieldValue as a property. The trick here is that you can change getValue() method declaration and declare that it returns the common interface or Object (I used Object but interface will work as well).

This solution doesn't require any custom serializers/deserializers and easy in maintenance.

Jackson: programmatically determine subtypes

Instead of ai.findSubTypes(ac) you may use mapper.getSubtypeResolver().collectAndResolveSubtypes(ac, config, ai).
I've not tested it with registered subtypes but looking at the code it should work.

Jackson polymorphic deserialization with dynamic types

After playing with Jackson for some time I came to the following solution. Works fine for me.

First we make everything polymorphic

@JsonTypeResolver(MyTypeResolver.class)
@JsonTypeIdResolver(MyTypeIdResolver.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, property = "@type")
public interface ObjectMixin {

}

ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.addMixIn(Object.class, ObjectMixin.class);

We create custom TypeResolver that only handles type serialization/deserilization for java.lang.Object.

public class MyTypeResolver extends StdTypeResolverBuilder {

@Override
public TypeSerializer buildTypeSerializer(SerializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
return useForType(baseType) ? super.buildTypeSerializer(config, baseType, subtypes) : null;
}

@Override
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType, Collection<NamedType> subtypes) {
return useForType(baseType) ? super.buildTypeDeserializer(config, baseType, subtypes) : null;
}

public boolean useForType(JavaType t) {
return t.isJavaLangObject();
}
}

TypeIdResolver in turn handles ID magic. In this example everything is hardcoded, in a real code it's looking much more nicer of course. :)

public class MyTypeIdResolver extends TypeIdResolverBase {

@Override
public String idFromValue(Object value) {
return getId(value);
}

@Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
return getId(value);
}

@Override
public JsonTypeInfo.Id getMechanism() {
return JsonTypeInfo.Id.CUSTOM;
}

private String getId(Object value) {
if (value instanceof ListWrapper.MyMapListWrapper) {
return "MyMap[]";
}

if (value instanceof ListWrapper.Child1ListWrapper) {
return "Child1[]";
}

if (value instanceof ListWrapper && !((ListWrapper) value).getValues().isEmpty()) {
return ((ListWrapper) value).getValues().get(0).getClass().getSimpleName() + "[]";
}

return value.getClass().getSimpleName();
}

@Override
public JavaType typeFromId(DatabindContext context, String id) throws IOException {
if (id.endsWith("[]")) {
if (id.startsWith("Child1")) {
return TypeFactory.defaultInstance().constructParametricType(ListWrapper.class, Child1.class);
}
if (id.startsWith("MyMap")) {
return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), ListWrapper.MyMapListWrapper.class);
}
}
if (id.equals("Child1")) {
return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), Child1.class);
}
if (id.equals("MyMap")) {
return TypeFactory.defaultInstance().constructSpecializedType(TypeFactory.unknownType(), MyMap.class);
}

return TypeFactory.unknownType();
}
}

To be able to handle {"@type: "...", "@values": ...} lists I have a ListWrapper class and subclasses. Todo: reimplement this using custom deserialization logic.

public class ListWrapper<T> {    
@JsonProperty("@values")
private List<T> values;

public static class MyMapListWrapper extends ListWrapper<MyMap> {
}

public static class Child1ListWrapper extends ListWrapper<Child1> {
}
}

It's possible to skip creation of subclasses but then the type information is added to every single element. java.lang.* classes come without type information of course.

Models are:

public class Parent {
private String prop;
private Child1 child1;
private MyMap map;
}

public class Child1 {
private int anInt;
}

The test code:

@Test
public void shouldDoTheTrick() throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
mapper.addMixIn(Object.class, ObjectMixin.class);

Parent parent = new Parent("Hello", new Child1(-1), new MyMap() {{
put("JustString", "JustValue");
put("List_With_All_MyMaps", new ListWrapper.MyMapListWrapper(new ArrayList<MyMap>() {{
add(new MyMap() {{
put("Key", "Value");
put("object", new Child1(2));
}});
add(new MyMap() {{
put("Key", "Value");
}});
}}));
put("List_With_All_Child1", new ListWrapper.Child1ListWrapper(new ArrayList<Child1>() {{
add(new Child1(41));
add(new Child1(42));
}}));
}});

String valueAsString = mapper.writeValueAsString(parent);

Parent deser = mapper.readValue(valueAsString, Parent.class);
assertEquals(parent, deser);
}

The JSON output:

{
"prop" : "Hello",
"child1" : {
"anInt" : -1
},
"map" : {
"JustString" : "JustValue",
"List_With_All_MyMaps" : {
"@type" : "MyMap[]",
"@values" : [ {
"Key" : "Value",
"object" : {
"@type" : "Child1",
"anInt" : 2
}
}, {
"Key" : "Value"
} ]
},
"List_With_All_Child1" : {
"@type" : "Child1[]",
"@values" : [ {
"anInt" : 41
}, {
"anInt" : 42
} ]
}
}
}

UPD: real implementation example https://github.com/sdl/dxa-web-application-java/commit/7a36a9598ac2273007806285ea4d854db1434ac5



Related Topics



Leave a reply



Submit