Deserialize List of polymorphic objects into Object field
Unfortunately deserializer accepts type from the declared type of the field. So if field is Object
then it will not deserialize to anything and leave it as Map
. There is no simple solution to this because deserializes has no information about what type it should be. One solution is custom mapper like one of the answers is, or you can use custom TaggedObject
class instead of Object
which is what I used for similar use case:
public class TaggedObject { @Expose private String type; @Expose private Object value; @Expose private Pair<TaggedObject, TaggedObject> pairValue; @SuppressWarnings("unchecked") public TaggedObject(Object v) { this.type = getTypeOf(v); if (v instanceof Pair) { this.pairValue = tagPair((Pair<Object, Object>) v); this.value = null; } else if ("long".equals(type)){ this.value = "" + v; this.pairValue = null; } else { this.value = v; this.pairValue = null; } } private static Pair<TaggedObject, TaggedObject> tagPair(Pair<Object, Object> v) { return new Pair<TaggedObject, TaggedObject>(TaggedObject.tag(v.first), TaggedObject.tag(v.second)); } private static String getTypeOf(Object v) { Class<?> cls = v.getClass(); if (cls.equals(Double.class)) return "double"; if (cls.equals(Float.class)) return "float"; if (cls.equals(Integer.class)) return "integer"; if (cls.equals(Long.class)) return "long"; if (cls.equals(Byte.class)) return "byte"; if (cls.equals(Boolean.class)) return "boolean"; if (cls.equals(Short.class)) return "short"; if (cls.equals(String.class)) return "string"; if (cls.equals(Pair.class)) return "pair"; return ""; } public static TaggedObject tag(Object v) { if (v == null) return null; return new TaggedObject(v); } public Object get() { if (type.equals("pair")) return new Pair<Object, Object>( untag(pairValue.first), untag(pairValue.second) ); return getAsType(value, type); } private static Object getAsType(Object value, String type) { switch (type) { case "string" : return value.toString(); case "double" : return value; case "float" : return ((Double)value).doubleValue(); case "integer": return ((Double)value).intValue(); case "long" : { if (value instanceof Double) return ((Double)value).longValue(); else return Long.parseLong((String) value); } case "byte" : return ((Double)value).byteValue(); case "short" : return ((Double)value).shortValue(); case "boolean": return value; } return null; } public static Object untag(TaggedObject object) { if (object != null) return object.get(); return null; }}
This is for google gson (that's why there are @Expose
annotations) but should work just fine with jackson. I did not include Pair
class but you can surely make your own based by the signature or omit it (I needed to serialize pairs).
If you know the type to convert to, you can use the method convertValue
of ObjectMapper
.
You can convert either a single value or the whole list.
Consider the following example:
public static void main(String[] args) throws IOException { ObjectMapper objectMapper = Jackson.newObjectMapper() .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE) .setDefaultPropertyInclusion(Include.NON_NULL); objectMapper.addMixIn(Animal.class, Mixin.class); Zoo zoo = objectMapper.readValue(JSON_STRING, Zoo.class); List<Animal> animals = objectMapper.convertValue(zoo.getAnimals(), new TypeReference<List<Animal>>(){}); for (Object animal : animals) { System.out.println(animal.getClass()); }}
For your comments, if you are unsure of the actual type that need to be processed, one option that can be useful is enabling Jackson's default typing (with the required precautions).
You can do it like this:
PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator .builder() .allowIfBaseType(Animal.class) .build();ObjectMapper mapper = new ObjectMapper();mapper.activateDefaultTyping(DefaultTyping.NON_FINAL, validator);
Another think you can do is define a custom Deserializer
for the field that contains your arbitrary information.
Instead of creating one from scratch, you can reuse some code.
When you define as Object
your property, Jackson will assign an instance of the class com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer
as itsDeserializer
.
In your case, this class will process first the array, so we obtain a List
as the main deserialization result.
This List
will be composed of instances of the class LinkedHashMap
. Why? Because in their implementation, for every JSON object in the array, they generate a LinkedHashMap
with keys, the JSON object property names in the order that they appear, and values, an String[]
with the JSON object property values.
This process is handled by the mapObject
method. If you have some kind of field that allow you to identify the type of Object
you are processing, maybe you can define a new Deserializer
that extends UntypedObjectDeserializer
and overwrites the mapObject
method to create, from the Map
returned by its parent, the actual, concrete, objects, just by applying Java Bean introspection and property setters.
This last approach can also be implemented with Converter
s. They are actually very well suited for this two phase process. The idea behind that is the same described in the last paragraph: you will receive a List
of Map
s, and if you are able to identify the concrete type, you only need to build the corresponding instances from the information received. You can even use ObjectMapper
for this last conversion step.
The requirement is that a property of type Object an be deserialized into the correct class.
One could consider using different routes for different types. If this is not an option, and since you have control over the serialized data and to stay as close as possible to your existing code, you could use a JsonDeserialize annotation.
The solution could be to add a 'response_type' to the JSON data.
Since you need to deserialize other types of objects besides list of animals, maybe a more generic name instead of animals make sense. Let's rename it from animals
to response
.
So instead of
private Object animals;
you will have:
private Object response;
Then your code slightly modified to the above points could look like this:
package jackson.poly;import com.fasterxml.jackson.databind.annotation.JsonDeserialize;public class Zoo { private String responseType; @JsonDeserialize(using = ResponseDeserializer.class) private Object response; public String getResponseType() { return responseType; } public Object getResponse() { return response; }}
The ResponseDeserializer could look like this:
package jackson.poly;import com.fasterxml.jackson.core.JsonParser;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.DeserializationContext;import com.fasterxml.jackson.databind.JsonDeserializer;import java.io.IOException;import java.util.Arrays;public class ResponseDeserializer extends JsonDeserializer<Object> { @Override public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { Zoo parent = (Zoo)deserializationContext.getParser().getParsingContext().getParent().getCurrentValue(); String responseType = parent.getResponseType(); if(responseType.equals("Animals[]")) { Animal[] animals = jsonParser.readValueAs(Animal[].class); return Arrays.asList(animals); } else if(responseType.equals("Facilities[]")) { Facility[] facilities = jsonParser.readValueAs(Facility[].class); return Arrays.asList(facilities); } throw new RuntimeException("unknown subtype " + responseType); }}
Using input data like this (note the response_type property):
private static final String JSON_STRING = "{\n" + " \"response_type\": \"Animals[]\",\n" + " \"response\": [\n" + " {\"name\": \"dog\"},\n" + " {\"name\": \"cat\"}\n" + " ]\n" + "}";
a test run of the above program would produce the following output on the debug console:
class jackson.poly.Dogclass jackson.poly.Cat
If you would use something like this as input instead (assuming the corresponding classes exist in a form similar to the animal classes):
private static final String JSON_STRING = "{\n" + " \"response_type\": \"Facilities[]\",\n" + " \"response\": [\n" + " {\"name\": \"house\"},\n" + " {\"name\": \"boat\"}\n" + " ]\n" + "}";
you would get
class jackson.poly.Houseclass jackson.poly.Boat
as output.