Reputation: 63
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:
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?Animal
class has to be defined as abstract
for this to work?Let me know if the code is not clear and I will fix it.
Upvotes: 3
Views: 6881
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.
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);
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.
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
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:
include
and property
fields declare that you want to use a property named type
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 @ToString
s):
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