Deserialize List of polymorphic objects into Object field

2.4k Views Asked by At

My REST endpoint returns Response object containing Object field. Everything is fine with serialization but when I started to write client for this API I encountered an issue with deserialization. I made this example code based on some questions/articles about polymorphic serialization with Jackson. It demonstrates the issue.

@Data
abstract class Animal {

    String name;
}

@Data
class Dog extends Animal {

    boolean canBark;
}

@Data
class Cat extends Animal {

    boolean canMeow;
}

@Data
public class Zoo {

    private Object animals;
}


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "name", visible = true)
@JsonSubTypes({
        @JsonSubTypes.Type(name = "dog", value = Dog.class),
        @JsonSubTypes.Type(name = "cat", value = Cat.class)
})
public class Mixin {

}

public class Main {

    private static final String JSON_STRING = "{\n"
            + "  \"animals\": [\n"
            + "    {\"name\": \"dog\"},\n"
            + "    {\"name\": \"cat\"}\n"
            + "  ]\n"
            + "}";

    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);
        for (Object animal : (Collection) zoo.getAnimals()) {
            System.out.println(animal.getClass());
        }
    }
}

What I expect(and what I have with List<Animals> as Zoo#animals type) in output:

class jackson.poly.Dog
class jackson.poly.Cat

What I have now with Object:

class java.util.LinkedHashMap
class java.util.LinkedHashMap

But I need to deserialize other types of objects besides list of animals. Help please!

5

There are 5 best solutions below

0
On BEST ANSWER

I express sincere gratitude to all who participated. I decided to go with a little bit introspective approach and wrote custom deserializer that looks inside first object in list to figure out if it needs to deserialize this as List.

public class AnimalsDeserializer extends JsonDeserializer<Object> {

    private Set<String> subtypes = Set.of("dog", "cat");

    @Override
    public Object deserialize(JsonParser parser, DeserializationContext ctx)
            throws IOException, JsonProcessingException {
        if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
            TreeNode node = parser.getCodec().readTree(parser);
            TreeNode objectNode = node.get(0);
            if (objectNode == null) {
                return List.of();
            }
            TreeNode type = objectNode.get("name");
            if (type != null
                    && type.isValueNode()
                    && type instanceof TextNode
                    && subtypes.contains(((TextNode) type).asText())) {
                return parser.getCodec().treeAsTokens(node).readValueAs(new TypeReference<ArrayList<Animal>>() {
                });
            }
        }
        return parser.readValueAs(Object.class);
    }
}

Maybe some checks are redundant. It works and it's all that matters :). Award goes to StephanSchlecht as author of most inspirational answer. Thanks again!

0
On

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.Dog
class 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.House
class jackson.poly.Boat

as output.

2
On

Without making the animals variable be of type List, its difficult to let object mapper understand that there are subtypes defined for animals.

If not, write your own serializer as below.

    class AnimalsDeserializer extends StdConverter<List<Animal>, Object> {

        @Override
        public Object convert(List<Animal> animals) {
            return animals;
        }
    }

Annotate animals with @JsonDeserialize.

    @Data
    class Zoo {

        @JsonDeserialize(converter = AnimalsDeserializer.class)
        private Object animals;
    }

Then it will definitely work.

8
On

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 Converters. 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 Maps, 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.

1
On

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).