Deserialize JSON with Jackson into Polymorphic Types - A Complete Example is giving me a compile error
Asked Answered
D

7

136

I am attempting to work through a tutorial from Programmer Bruce that is supposed to allow the deserialization of polymorphic JSON.

The complete list can be found here Programmer Bruce tutorials (Great stuff btw)

I have worked through the first five with no problems but I have hit a snag on the last one (Example 6), which of course is the one I really need to get working.

I am getting the following error at compile time

The method readValue(JsonParser, Class) in the type ObjectMapper is not applicable for the arguments (ObjectNode, Class<capture#6-of ? extends Animal>)

and it's being caused by the chunk of code

  public Animal deserialize(  
      JsonParser jp, DeserializationContext ctxt)   
      throws IOException, JsonProcessingException  
  {  
    ObjectMapper mapper = (ObjectMapper) jp.getCodec();  
    ObjectNode root = (ObjectNode) mapper.readTree(jp);  
    Class<? extends Animal> animalClass = null;  
    Iterator<Entry<String, JsonNode>> elementsIterator =   
        root.getFields();  
    while (elementsIterator.hasNext())  
    {  
      Entry<String, JsonNode> element=elementsIterator.next();  
      String name = element.getKey();  
      if (registry.containsKey(name))  
      {  
        animalClass = registry.get(name);  
        break;  
      }  
    }  
    if (animalClass == null) return null;  
    return mapper.readValue(root, animalClass);
  }  
} 

Specifically by the line

return mapper.readValue(root, animalClass);

Has anyone run into this before and if so, was there a solution?

Delmadelmar answered 21/5, 2015 at 0:12 Comment(5)
What Jackson version are you using, the tutorial assumes Jackson 1.x, also any reason why not prefer annotations based deserialization for polymorphic instances?Remark
I am using 2.5. I can see if a downgrade to 1.X will solve the issue. Also, can you recommend a tutorial/example that might show the use of annotations to handle this issue?Delmadelmar
Yeah, I wouldn't recommend you to downgrade, I will happily give an example working.Remark
Here is another article well explaining the different ways to perform polymorphic serialization / deserialization: octoperf.com/blog/2018/02/01/polymorphism-with-jacksonDeify
I've just added an (arguably) simpler solution that handles deserializing to different types based on the the presence of a property: https://mcmap.net/q/168567/-deserializing-polymorphic-types-with-jackson-based-on-the-presence-of-a-unique-propertyMikamikado
R
208

As promised, I'm putting an example for how to use annotations to serialize/deserialize polymorphic objects, I based this example in the Animal class from the tutorial you were reading.

First of all your Animal class with the Json Annotations for the subclasses.

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),

    @JsonSubTypes.Type(value = Cat.class, name = "Cat") }
)
public abstract class Animal {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

Then your subclasses, Dog and Cat.

public class Dog extends Animal {

    private String breed;

    public Dog() {

    }

    public Dog(String name, String breed) {
        setName(name);
        setBreed(breed);
    }

    public String getBreed() {
        return breed;
    }

    public void setBreed(String breed) {
        this.breed = breed;
    }
}

public class Cat extends Animal {

    public String getFavoriteToy() {
        return favoriteToy;
    }

    public Cat() {}

    public Cat(String name, String favoriteToy) {
        setName(name);
        setFavoriteToy(favoriteToy);
    }

    public void setFavoriteToy(String favoriteToy) {
        this.favoriteToy = favoriteToy;
    }

    private String favoriteToy;

}

As you can see, there is nothing special for Cat and Dog, the only one that know about them is the abstract class Animal, so when deserializing, you'll target to Animal and the ObjectMapper will return the actual instance as you can see in the following test:

public class Test {

    public static void main(String[] args) {

        ObjectMapper objectMapper = new ObjectMapper();

        Animal myDog = new Dog("ruffus","english shepherd");

        Animal myCat = new Cat("goya", "mice");

        try {
            String dogJson = objectMapper.writeValueAsString(myDog);

            System.out.println(dogJson);

            Animal deserializedDog = objectMapper.readValue(dogJson, Animal.class);

            System.out.println("Deserialized dogJson Class: " + deserializedDog.getClass().getSimpleName());

            String catJson = objectMapper.writeValueAsString(myCat);

            Animal deseriliazedCat = objectMapper.readValue(catJson, Animal.class);

            System.out.println("Deserialized catJson Class: " + deseriliazedCat.getClass().getSimpleName());



        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Output after running the Test class:

{"@type":"Dog","name":"ruffus","breed":"english shepherd"}

Deserialized dogJson Class: Dog

{"@type":"Cat","name":"goya","favoriteToy":"mice"}

Deserialized catJson Class: Cat

Remark answered 22/5, 2015 at 0:47 Comment(20)
what if I get the object from Retrofit without the @type info?Currajong
Is it possible to do this without using the @JsonSubTypes annotation? So that subclasses can be added in other packages after the original class has been authored?Outlive
Do you know if this could work if the SubTypes are in different Maven projects ? i.e: Cat, Dog are each inside a different Maven project than Animal. I have a problem with circular dependency and can't figure a way out of it.Helve
Could you do a similar thing if Animal was an interface and not an abstract class? Also, is it correct that name property on abstract Animal class is no way related to name = "Dog" in annotation?Stile
@KamalJoshi yes, you can do the same if Animal is an interface. Just tested by changing the Animal to an interface and the same main method worked. About your second question, it's correct that the name is not related to the Dog in the annotation, name is just a field of the serialized class, probably to avoid confusion I could've use petName int the Animal class instead of name. The jackson annotation name field, is the value to set to the serialized Json as identifier. HTH.Remark
Great explanation, but I'm still having problems in the case when I know I will get just a subtype and I want to deserialize it, but the property "type" is not in the json, which is understandable, as now I'm 100% sure that the list is just of the subtypes. I had to use the defaultImpl in the parent case but is not really good as I may as well get a list of the other subtype. Any ideas? bascically I'm getting a list of Dogs, where the @type is missing.Whitleather
@HDave I believe you can implement your own @JsonTypeIdResolver which would use Reflection to determine the list of possible subclasses and bind your @type to their names. An example implementation can be found here: thomaskeller.biz/blog/2013/09/10/…Carbine
@HDave I've set up a gist with the @JsonTypeIdResolver which (probably) does what you need: gist.github.com/root-talis/36355f227ff5bb7a057ff7ad842d37a3Carbine
Does the abstract class (or child classes) have to implement Serializable for this to work?Sighted
@user1300959 no they don't need to implement Serializable to make this example work.Remark
What if the abstract type I am using belongs to a library where I am unable to change the class in anyway. Can I set some custom properties inside my ObjectMapper or configuration that will do the same?Severable
Since when does a JSON object tell you what type it is? I have never seen that pattern, and it's a good thing because it opens-up an attack vector anyway. Polymorphism is almost always inferred from the type's canonical fields.Mariande
this can't be an approach. Where in the world a base abstract class knows about each and every possible subclasses? This should be bad by design.Manus
Textbook example of how to show an example!! You saved me a lot of time! :):)Wendolynwendt
@wheelerswebservices Lookup Jackson's mixin annotations, using mix-in annotations you can accomplish adding annotations to classes you are not in control of.Leukemia
@Manus Kotlin sealed classes disagrees with you: kotlinlang.org/docs/sealed-classes.html It has actually turned out to be a very useful feature in Kotlin.Leukemia
@SimonForsberg thanks for the reference but I'm not saying that you could do that. I'm saying that's not a good design basically because abstract class are used for that, being abstract without knowing the details of what their subclasses do. Still bad idea and still couples your code.Manus
@Manus My point was that in Kotlin, sealed classes has a MySealedClass::class.sealedSubclasses which can tell you which the subclasses are (Although maybe you're ok with figuring it out with reflection?) The whole point of sealed classes in Kotlin is to restrict which subclasses exist. And trust me, they are very useful and I think they are a good design choice in many cases. As for this specific question/code, I'd agree that @JsonSubTypes is a bad way to do it and I would use @JsonTypeName on each subclass myself.Leukemia
What if one of the subclasses is declared and used in the tests? For example if we have the following hierarchy: - main - interface Animal - class DefaultAnimal extends Animal - test - class TestDefaultAnimal extends AnimalAnaconda
can we use this Animal as a requestBody which can be cat to subclasses ? I have problem with making this work, object does not have the properties of sub typesCaulescent
U
41

Whereas @jbarrueta answer is perfect, in the 2.12 version of Jackson was introduced a new long-awaited type for the @JsonTypeInfo annotation, DEDUCTION.

It is useful for the cases when you have no way to change the incoming json or must not do so. I'd still recommend to use use = JsonTypeInfo.Id.NAME, as the new way may throw an exception in complex cases when it has no way to determine which subtype to use.

Now you can simply write

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
    @JsonSubTypes.Type(Dog.class),
    @JsonSubTypes.Type(Cat.class) }
)
public abstract class Animal {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

And it will produce {"name":"ruffus", "breed":"english shepherd"} and {"name":"goya", "favoriteToy":"mice"}

Once again, it's safer to use NAME if some of the fields may be not present, like breed or favoriteToy.

Unwished answered 12/1, 2021 at 13:47 Comment(5)
This is the most suitable answer, if Jackson 2.12+ is used.Ubiquitarian
Didn't knew about that. I guess it's gotta be a bit slower since it needs some analysing but I like the simplicity. I would still use JsonSubTypes({ Type(value = SomeType.class, name = "SOMENAME") to be safe though.Luther
Thanks for this. Perfect example and almost exactly what I needed. Is there any way to incorporate using the object itself as a wrapper object? In your example, I would want to see a JSON wrapper object around each Animal you created, so that one is wrapped with "Dog:{}", and the other "Cat:{}"... Is this possible? I tried using include = JsonTypeInfo.As.WRAPPER_OBJECT, on both the abstract and subclasses to no avail.Clotildecloture
@JackF. Sadly, I don't know a way to address your problem as is without writing a custom deserializer thingie, which is painful. First way is to create new dto wrappers, and deserialize them. Second way is more hacky, but you can create one generified dto wrapper, put @JsonAlias on the wrapped field so it would match both "Cat" and "Dog", and laugh like an evil madman. I'd go with the first style, it is a bit more tedious to support, but it has its fields explicitly declared. Also when someone might decide to add an additional field into the wrapper dto, it'd be easier for them.Unwished
Also, stackoverflow mentions a lot of times, that comments are not for discussions, and your question feels more like a separate question, so I'd recommend writing it as a stand-alone post. You'll also receive better answers than I could do. :DUnwished
D
33

You need only one line before the declaration of the class Animal for correct polymorphic serialization/deserialization:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public abstract class Animal {
   ...
}

This line means: add a meta-property on serialization or read a meta-property on deserialization (include = JsonTypeInfo.As.PROPERTY) called "@class" (property = "@class") that holds the fully-qualified Java class name (use = JsonTypeInfo.Id.CLASS).

So, if you create a JSON directly (without serialization) remember to add the meta-property "@class" with the desired class name for correct deserialization.

More information here

Divorce answered 28/8, 2019 at 6:59 Comment(3)
I know i'm late to the party will this only work as long as the app serializing the json is also the same app de-serializing it? If you need to serialize an object from an app and de-serialize it elsewhere aren't you better using custom name->class mapping with JsonSubTypes? That way, whatever app is deserializing the json can have his custom mapping with the name given in JsonSubTypes. In other words, the other app can use it's own classes to deserialize. (unless of course the class included in the json is the simple name but I don't see how it could work.)Luther
Hi @Luther what you say is certainly something to take into account when working with differents apps. I tried to answer the most common use case.Divorce
Indeed this solution is shorter but unfortunately Sonar reports here java:S4544 vulnerability with the risk of remote code execution. I'll go with the accepted answer.Berm
P
6

A simple way to enable polymorphic serialization / deserialization via Jackson library is to globally configure the Jackson object mapper (jackson.databind.ObjectMapper) to add information, such as the concrete class type, for certain kinds of classes, such as abstract classes.

To do that, just make sure your mapper is configured correctly. For example:

Option 1: Support polymorphic serialization / deserialization for abstract classes (and Object typed classes)

jacksonObjectMapper.enableDefaultTyping(
    ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); 

Option 2: Support polymorphic serialization / deserialization for abstract classes (and Object typed classes), and arrays of those types.

jacksonObjectMapper.enableDefaultTyping(
    ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); 

Reference: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization

Peerless answered 1/2, 2017 at 14:54 Comment(5)
where should i put this (im using hibernate)?Multinational
In a context where the ObjectMapper is defined, and before it is used. For example if you are using a static ObjectMapper, you can add a static paragraph and set these values there.Peerless
i'm not using objectmapper. It just just works without any definition.Multinational
The link you provided seems to be broken... could it be that now the URL is github.com/FasterXML/jackson-docs/wiki/…?Efrainefram
It seems that the link SJuan76 provided is working now, however the link you've provided contains the same information.Peerless
B
3

Handling polymorphism is either model-bound or requires lots of code with various custom deserializers. I'm a co-author of a JSON Dynamic Deserialization Library that allows for model-independent json deserialization library. The solution to OP's problem can be found below. Note that the rules are declared in a very brief manner.

public class SOAnswer {
    @ToString @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static abstract class Animal {
        private String name;    
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Dog extends Animal {
        private String breed;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Cat extends Animal {
        private String favoriteToy;
    }
    
    
    public static void main(String[] args) {
        String json = "[{"
                + "    \"name\": \"pluto\","
                + "    \"breed\": \"dalmatian\""
                + "},{"
                + "    \"name\": \"whiskers\","
                + "    \"favoriteToy\": \"mouse\""
                + "}]";
        
        // create a deserializer instance
        DynamicObjectDeserializer deserializer = new DynamicObjectDeserializer();
        
        // runtime-configure deserialization rules; 
        // condition is bound to the existence of a field, but it could be any Predicate
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("breed"),
                DeserializationActionFactory.objectToType(Dog.class)));
        
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("favoriteToy"),
                DeserializationActionFactory.objectToType(Cat.class)));
        
        List<Animal> deserializedAnimals = deserializer.deserializeArray(json, Animal.class);
        
        for (Animal animal : deserializedAnimals) {
            System.out.println("Deserialized Animal Class: " + animal.getClass().getSimpleName()+";\t value: "+animal.toString());
        }
    }
}

Maven depenendency for pretius-jddl (check newest version at maven.org/jddl:

<dependency>
  <groupId>com.pretius</groupId>
  <artifactId>jddl</artifactId>
  <version>1.0.0</version>
</dependency>
Both answered 21/12, 2020 at 8:58 Comment(0)
W
0

If the name of the existing property is not equal to name, you can use the annotation value EXISTING_PROPERTY

If the property name is for example type instead of name, you can use this annotation:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
              include = JsonTypeInfo.As.EXISTING_PROPERTY,
              property = "type")

See also https://mcmap.net/q/168568/-jackson-polymorphic-deserialization-based-on-enum

Weatherby answered 22/11, 2021 at 12:6 Comment(0)
A
-2

If using the fasterxml then,

these changes might be needed

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;

in main method--

use

SimpleModule module =
  new SimpleModule("PolymorphicAnimalDeserializerModule");

instead of

new SimpleModule("PolymorphicAnimalDeserializerModule",
      new Version(1, 0, 0, null));

and in Animal deserialize() function, make below changes

//Iterator<Entry<String, JsonNode>> elementsIterator =  root.getFields();
Iterator<Entry<String, JsonNode>> elementsIterator = root.fields();

//return mapper.readValue(root, animalClass);
return  mapper.convertValue(root, animalClass); 

This works for fasterxml.jackson. If it still complains of the class fields. Use the same format as in the json for the field names (with "_" -underscore). as this
//mapper.setPropertyNamingStrategy(new CamelCaseNamingStrategy()); might not be supported.

abstract class Animal
{
  public String name;
}

class Dog extends Animal
{
  public String breed;
  public String leash_color;
}

class Cat extends Animal
{
  public String favorite_toy;
}

class Bird extends Animal
{
  public String wing_span;
  public String preferred_food;
}
Archiepiscopal answered 19/8, 2016 at 6:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.