Jackson 3.1.1 Missing Type ID of Subtype Exception for Polymorphic Type of List?

Sarah Szabo picture Sarah Szabo · Aug 5, 2019 · Viewed 15.6k times · Source

I have a concrete object of an abstract class that I've made.

I'm using annotations on the abstract class and the daughter class, but the JSON output doesn't look right and I keep getting an exception on deserialiation.

I'm still learning Jackson, and unfortunatly a lot of the tutorials on the subject are outdated.

Here are the classes with my object mapper at the bottom:

Abstract Class:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "BTRFSPhysicalLocationItem")
@JsonSubTypes({
    @Type(name = "Snapshot", value = Snapshot.class),
    @Type(name = "Backup", value = Backup.class),
    @Type(name = "Subvolume", value = Subvolume.class)})
public abstract class BTRFSPhysicalLocationItem {

    private final String name;
    @JsonProperty
    private final Path location;

    /**
     * Makes a new subvolume instance with the specified location. May or may
     * not actually exist.
     *
     * @param location The location of the subvolume
     */
    @JsonCreator
    protected BTRFSPhysicalLocationItem(@JsonProperty(value = "location") Path location) {
        this.location = location;
        this.name = location.getFileName().toString();
    }

Concrete Class:

public class Subvolume extends BTRFSPhysicalLocationItem {

    /**
     * Creates a new subvolume with the specified path.The subvolume may or may
     * not exist at this point.
     *
     * @param location
     */
    @JsonCreator
    public Subvolume(@JsonProperty Path location) {
        super(location);
    }

Objectmapper block:

ObjectMapper MAPPER = new ObjectMapper();
List<Subvolume> SUBVOLUME_LIST = new ArrayList<>();
//Subvolume is populated by other methods it's not worth showing them here
System.out.println("\n\n");
                MAPPER.writeValue(System.out, SUBVOLUME_LIST);
                System.out.println("\n\n\n");
                var s = MAPPER.writeValueAsString(SUBVOLUME_LIST);
                List<Subvolume> list = MAPPER.readValue(s, new TypeReference<List<Subvolume>>() {
                });

Output JSON: [{"name":"WanderingEcho","location":"file:///home/sarah/NetBeansProjects/WanderingEcho/"}]

Exception:

Aug 05, 2019 4:55:14 PM com.protonmail.sarahszabo.wanderingecho.btrfs.BTRFS <clinit>
SEVERE: null
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class com.protonmail.sarahszabo.wanderingecho.btrfs.subvolume.Subvolume]: missing type id property 'BTRFSPhysicalLocationItem'
 at [Source: (String)"[{"name":"WanderingEcho","location":"file:///home/sarah/NetBeansProjects/WanderingEcho/"}]"; line: 1, column: 89] (through reference chain: java.util.ArrayList[0])

Other answers such as Jackson deserialization of polymorphic types suggest using the ObjectMapper methods, but according to the tutorials I've read, this shouldn't be necessary.

Not sure what I'm doing wrong with the annotations.

Answer

m.antkowicz picture m.antkowicz · Aug 5, 2019

In your json you are missing the BTRFSPhysicalLocationItem - just think about it - how Jackson should know what implementation do you expect?

It should look like

[
    {
        "BTRFSPhysicalLocationItem": "Subvolume",
        "name": "WanderingEcho",
        "location":"file:///home/sarah/NetBeansProjects/WanderingEcho/"
    }
]

if you want Jackson to include the type when serializing the object you also need to add JsonTypeInfo.As.PROPERTY as include as a parameter of @JsonTypeInfo annotation like

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "BTRFSPhysicalLocationItem")

EDIT ok seems that I figured this out - your problem with not generating properties is connected to Java type erasure and well described here.

tldr; Jackson has problem with underestanding type - seems List<Subvolume> for him does not providing the type of elements. Because he doesn't know the type he won't serialize it

The solution is to create a helper class that will extends ArrayList<Subvolume> like

public class SubvolumeList extends ArrayList<Subvolume> {}

Additionaly you have a few issues in your code - one is that you should use use = JsonTypeInfo.Id.NAME in @JsonTypeInfo if you want to deserialize by property value (like Subvolume) not by package/class string literal

Second is that you should name @JsonProperty on @JsonCreator parameter list like @JsonProperty(value = "location")