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
Error While Running Allure Command Line Generate Command
Regular Expression to Validate Username
Unit Testing Private Functions in Junit With Mockito
How to Specify the Required Java Version in a Gradle Build
How to Autowire a Component Which Is Having Constructor With Arguments in Springboot Application
Launching Spring Application Address Already in Use
How to Upload a Document to Sharepoint With Java
Using an Empty Keystore Password Used to Be Possible
Passing Data into HTML from Java Via Spring
How to Return Different Results When Calling the Same Mocked Method
Saveastextfile() to Write the Final Rdd as Single Text File - Apache Spark
Map Json String Column of a JPA Entity to Java Object Automatically
Mailconnectexception: Couldn't Connect to Host, Port: Smtp.Sendgrid.Net
How to Remove Duplicate and Sort Objects from Jsonarray Using Java