Javed
Javed

Reputation: 63

How to configure Jackson to deserialize List containing named types annotated on Base Class with @JsonTypeInfo and JsonSubTypes?

I have researched for the best way to implement Objects classes with inheritance and contained within an object as a List.

If you know any link which handles the scenario and provides the correct answer I will be happy to accept this as duplicate and close this item. I have following classes:

Zoo

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.List;
import lombok.*;
@Getter
@Setter
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
    public class Zoo {
        private String zooName;
        private String zooLocation;
        @JsonInclude(JsonInclude.Include.NON_EMPTY)
        private @Singular List<Animal> animals;
}

Animal

@Getter
@Setter
@Builder(builderMethodName = "parentBuilder")
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NONE,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type",
    defaultImpl = Animal.class
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Elephant.class, name = "elephant")
})
public class Animal{
    private String type;
    private String nameTag;
}

Dog

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonPropertyOrder({ "type", "nameTag"})
@JsonTypeName("dog")
public class Dog extends Animal{
    private String barkingPower;
}

Elephant

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonPropertyOrder({ "type", "nameTag"})
public class Elephant extends Animal{
    private String weight;
}

ModelHelper

public final class ModelHelper {
    private static final ObjectMapper MAPPER = new ObjectMapper();        
        .configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false)        
        .setSerializationInclusion(JsonInclude.Include.NON_NULL)    
        .enableDefaultTypingAsProperty(
            ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT,    "type");
    public static <T> T fromJson(@NotNull final String json, final Class<T> objectClass) {
          try {
            return MAPPER.readValue(json, objectClass);
        } catch (Exception e) {
            final String message = String.format("Error de-serialising JSON '%s' to object of %s",
                    json, objectClass.getName());
                log.warn(message, e);
            return null;
        }
    }
}

And I am using the ModelHelper to deserialize the JSON to an Object:

String json = "{\"zooName\":\"Sydney Zoo\",\"zooLocation\":\"Sydney\",\"animals\":[{\"type\":\"dog\",\"nameTag\":\"Dog1\",\"barkingPower\":\"loud\"},{\"type\":\"elephant\",\"nameTag\":\"Elephant1\",\"weight\":\"heavy\"}]}";
mapper.readValue(json, Zoo.class);

Currently the deserialization of Zoo only comes back with Animal attributes and not with Dog or Elephant attributes.

My Questions are:

  1. What I can gather is deserialization is not working because the getter in Zoo for List<Animal> is of base class type and deserialization is not able to work out how to create a Dog or Elephant, and based on signature it generates Animal. But I would have thought that by putting JsonTypeName and JsonSubTypes annotation I have marked the relevant subclasses. is this the case?
  2. Does Animal class has to be defined as abstract for this to work?
  3. Is the only way to get this to work is to implement custom deserialization for class Zoo? Is this a good example to follow: https://www.sghill.net/how-do-i-write-a-jackson-json-serializer-deserializer.html?

Let me know if the code is not clear and I will fix it.

Upvotes: 3

Views: 6881

Answers (2)

Javed
Javed

Reputation: 63

I solved the issue following the answer in the stackoverflow posted answer by @programerB (Thanks Bruce): Dynamic polymorphic type handling with Jackson

I ended up implementing it using example 6.

  1. What I can gather is deserialization is not working because the getter in Zoo for List is of base class type and deserialization is not able to work out how to create a Dog or Elephant, and based on signature it generates Animal. But I would have thought that by putting JsonTypeName and JsonSubTypes annotation I have marked the relevant subclasses. is this the case?

Answer: The problem I see is that the type coming in json has " as part of string when JSON Node value is retrieved. And I think this is why the automatic handling based on JsonTypeName was not able to identify the right subclass.

String json = "{\"zooName\":\"Sydney Zoo\",\"zooLocation\":\"Sydney\",\"animals\":[{\"type\":\"dog\",\"nameTag\":\"Dog1\",\"barkingPower\":\"loud\"},{\"type\":\"elephant\",\"nameTag\":\"Elephant1\",\"weight\":\"heavy\"}]}";

If you see example 6 in programmer bruce solution. In order to identify the right subclass following code block is used:

String name = element.getKey();
if (registry.containsKey(name))
{
  animalClass = registry.get(name);
  break;
}

This is where I used the element.getValue to identify the right subclass. I had to strip off the quotes("), before comparing the value. Also if you are using fasterxml. You have to replace the following

return mapper.readValue(root, animalClass);

with

return mapper.readValue(root.toString(), animalClass);
  1. Does Animal class has to be defined as abstract for this to work?

Answer: Animal class is not required to be abstract. A non-abstract class can be the base class. In my example I have even instantiated this class.

  1. Is the only way to get this to work is to implement custom deserialization for class Zoo? Is this a good example to follow: https://www.sghill.net/how-do-i-write-a-jackson-json-serializer-deserializer.html?

Answer: Based on what I have observed custom deserialization was the best/neat way to handle this. The custom deserializer was required for the Animal class and not the Zoo Class. As mentioned above the link to follow is: http://programmerbruce.blogspot.com.au/2011/05/deserialize-json-with-jackson-into.html.

I can mark this as duplicate of the question where it is answered by programmerbruce but I think the way I have explained the issue and provided the solution will probably help others.

Upvotes: 1

user1803551
user1803551

Reputation: 13427

You're mixing about 3 ways to deserialize polymorphic types, which is not surprising considering how many iterations Jackson has gone through.

You marked Animal with

@JsonTypeInfo(use = JsonTypeInfo.Id.NONE, include = JsonTypeInfo.As.PROPERTY, property = "type")

This tells Jackson 2 contradicting things:

  • The include and property fields declare that you want to use a property named type
  • The use field declares that no ID mechanism should be used, thus ignoring your type info effectively.

The simplest "fix" is to use JsonTypeInfo.Id.NAME instead, which will output (I've added Lombok's @ToStrings):

Zoo(zooName=Sydney Zoo, zooLocation=Sydney, animals=[Dog(barkingPower=loud), Elephant(weight=heavy)])

So now you have proper type identification. Note that in this case the @JsonTypeName annotations on the subclasses are not needed since you're already specifying a way to resolve types: @JsonTypeInfo tells the deserializer to look for a type property and @JsonSubTypes tells it to map its value (the name property) to its class (the value property). You also don't need to specify any ObjectMapper configuration.

What is @JsonTypeName used for then? It replaced the name in @JsonSubTypes:

@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Elephant.class) // no name
})
public class Animal { ... }

public class Dog extends Animal { ... }

@JsonTypeName("elephant") // name is here
public class Elephant extends Animal { ... }

Useful, for example, if you don't have access to the superclass.

Another way to do all this without less annotations (again, useful if you don't have access to the classes) is by configuring the mapper. Instead of @JsonSubTypes you can use

MAPPER.registerSubtypes(new NamedType(Dog.class, "dog"), new NamedType(Elephant.class, "elephant"));

which includes the type name-class mapping here.

There are most probably more ways to mix configurations, but this demonstrates the point well enough and answers your questions.

The problem is that I could not deserialize the superclass properties along with the subclass properties. It's either one or the other. Perhaps a custom deserializer can indeed solve that.

Upvotes: 1

Related Questions