FasterXML / jackson-databind

General data-binding package for Jackson (2.x): works on streaming API (core) implementation(s)
Apache License 2.0
3.53k stars 1.38k forks source link

Deserialize JSON into Polymorphic Types #3292

Closed nurs-lr closed 3 years ago

nurs-lr commented 3 years ago

When attempting to deserialize the JSON array to the concrete classes that are the part of a polymorphic type hierarchy, a JsonParseException is thrown due to an unrecognized field.

The error:

Caused by: com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'ownerName': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')

Dummy Example:

Trying to perform deserialization using @JsonTypeInfo and @JsonSubTypes.


public class Owner {
     private String id;
     private String ownerName;
}

public abstract class Base {

     @JsonUnwrapped 
     protected Owner owner;
}

@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Cat.class, name = "cat"),
    @JsonSubTypes.Type(value = Dog.class, name = "dog")})
public abstract class Animal extends Base {

     private String type;

     public Animal (Owner owner, String type) {
         this.owner = owner;
         this.type = type;
    }
}

public class Cat extends Animal {

    private String name;
    private String sound;

    // Cat is NOT supposed to have Owner
    public Cat (String type, String name, String sound){
         super(null, type);
         this.name = name;
         this.sound = sound;
    }
}

public class Dog extends Animal {

    private String breed;
    private String speed;

    // Dog is supposed to have Owner
    public Dog (Owner owner, String type, String breed, String speed){
         super(owner, type);
         this.breed = breed;
         this.speed = speed;
    }
}
 [
        {
        "ownerName": "Mike",
        "id": "04f19621-a5ce-4159-9a39-67172a0d0dc7",
        "breed": "Labrador",
        "speed": "20 km/h",
        "type": "Dog"
    },
    {
        "name": "Mars",
        "sound": "Meow",
        "type": "Cat"
    }
 ]

// stringified version of the json example above
final String jsonString = "[{\"ownerName\":\"Mike\",\"id\":\"04f19621-a5ce-4159-9a39-67172a0d0dc7\",\"breed\":\"Labrador\",\"speed\":\"20 km\/h\",\"type\":\"Dog\"},{\"name\":\"Mars\",\"sound\":\"Meow\",\"type\":\"Cat\"}]";

List<Animal> animals = null

try {
     animals = Arrays.asList(new ObjectMapper().readValue(jsonString, Animal[].class)); // throws JsonParseException: Unrecognized token 'ownerName':

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

What would be the solution to deserialize with the properties which are coming from the parent class? (trying not to write CustomDeserializer)

cowtowncoder commented 3 years ago

Your declarations is incorrect: you cannot to use @JsonTypeInfo in subtype if reading as base type: declaration MUST BE on Base, not Animal, if target type is Base. This is the problem here.

nurs-lr commented 3 years ago

Update: I've used the same setup through RestAssured API and it worked as expected:

List<Animal> animals = response.as(new TypeRef<>() {}); // no exception 

@cowtowncoder Thanks for looking into it!