Reputation: 301
I have a data structure with some strongly-typed fields and some loosely-typed fields. Some of those fields are Collections that can be any deep nested.
The example JSON
{
"prop": "Hello", //strongly-typed
"child1": {
"anInt": -1
},
"map": { // here magic begins
"JustString": "JustValue", // we may store any Object in this map
"Item_With_Type": {
"@type": "MyMap", // some of them tell their type and we need to rely on it
"Custom": "Value"
},
"List_With_All_Child1": {
"@type": "MyMap[]", // lists define the type of all values in it in this way
"@values": [
{
"Key": "Value", // MyMap is a Map
"Child1": { // of <? extends Object>
"anInt": 2
}
},
{
"Key": "Value"
}
]
}
}
}
I want to map this on
public static class Parent {
private String prop;
private Child1 child1;
private MyMap<?> map;
}
public static class Child1 {
private int anInt;
}
public static class MyMap<T> extends HashMap<String, T> implements Map<String, T> {
}
(accessors omitted)
Basically I need kind of Data Binder which Jackson will ask about a type each time it tries to resolve a type for any field saving context, and if this data binder didn't find anything application-specific, Jackson should fallback to default type resolving.
Any ideas how to achieve this?
Upvotes: 3
Views: 4639
Reputation: 301
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
Upvotes: 3