Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson Polymorphism: Nested Subtypes

Is it possible to use multiple @JsonSubType annotations in a nested fashion?

For example, imagine the following classes:

@Data
@JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="species", use=Id.NAME, visible=true)
@JsonSubTypes({
  @Type(name="Dog", value=Dog.class)
  @Type(name="Cat", value=Cat.class)
})
public abstract class Animal {
  private String name;
  private String species;
}
@Data
@JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="breed", use=Id.NAME, visible=true)
@JsonSubTypes({
  @Type(name="Labrador", value=Labrador.class)
  @Type(name="Bulldog", value=Bulldog.class)
})
public abstract class Dog extends Animal {
  private String breed;
}
@Data
public class Cat extends Animal {
  private boolean lovesCatnip;
}
@Data
public class Labrador extends Dog {
  private String color;
}
@Data
public class Bulldog extends Dog {
  private String type; // "frenchy", "english", etc..
}

If I use an object mapper, I can successfully map a Bulldog to JSON, however, when trying to read the resulting JSON and read it back in, I get an error like the following:

Can not construct instance of com.example.Dog abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

Is it possible to get Jackson to work with such subtyping? Would I need to create a custom deserializer for each subclass?

EDIT:

I've modified the classes above slightly from the original posting. I added a Cat class and had that and Dog extend from Animal.

Here is a sample JSON that can be created using the ObjectMapper::writeValueAsString:

{
  "name": null,
  "species": "Dog",
  "breed": "Bulldog",
  "type": "B-Dog"
}
like image 656
naydichev Avatar asked Oct 19 '25 12:10

naydichev


2 Answers

The following works if I use @JsonTypeInfo and a similar set up to yours. Maybe your problem is in your deserialization code, so have a look at this:

public class MyTest {

    @Test
    public void test() throws IOException {
        final Bulldog bulldog = new Bulldog();
        bulldog.setBreed("Bulldog");
        bulldog.setType("B-Dog");

        final ObjectMapper om = new ObjectMapper();
        final String json = om.writeValueAsString(bulldog);
        final Dog deserialized = om.readValue(json, Dog.class);
        assertTrue(deserialized instanceof Bulldog);

    }

    @JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "species", use = Id.NAME, visible = true)
    @JsonSubTypes({
            @Type(name = "Dog", value = Dog.class),
            @Type(name = "Cat", value = Cat.class)
    })

    public static abstract class Animal {

        private String name;
        private String species;
    }

    @JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "breed", use = Id.NAME, visible = true)
    @JsonSubTypes({
            @Type(name = "Labrador", value = Labrador.class),
            @Type(name = "Bulldog", value = Bulldog.class)
    })
    public static abstract class Dog {

        private String breed;

        public String getBreed() {
            return breed;
        }

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

    public static abstract class Cat {

        private String name;
    }

    public static class Labrador extends Dog {

        private String color;

        public String getColor() {
            return color;
        }

        public void setColor(final String color) {
            this.color = color;
        }
    }

    public static class Bulldog extends Dog {

        private String type; // "frenchy", "english", etc..

        public String getType() {
            return type;
        }

        public void setType(final String type) {
            this.type = type;
        }
    }
}

EDITed for the updated question: If you can use the same property (in the following code the hidden property "@class") for the inheritance hierarchy, it works:

    @Test
public void test() throws IOException {
    final Bulldog bulldog = new Bulldog();
    // bulldog.setSpecies("Dog");
    // bulldog.setBreed("Bulldog");
    bulldog.setType("B-Dog");

    final ObjectMapper om = new ObjectMapper();
    final String json = om.writeValueAsString(bulldog);
    final Animal deserialized = om.readValue(json, Animal.class);
    assertTrue(deserialized instanceof Bulldog);

}

@JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
@JsonSubTypes({
        @Type(Dog.class),
        @Type(Cat.class)
})
public static abstract class Animal {

}

@JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
@JsonSubTypes({
        @Type(name = "Labrador", value = Labrador.class),
        @Type(name = "Bulldog", value = Bulldog.class)
})
public static abstract class Dog
        extends Animal {

}

If you want to set the animal type (e.g. to compute species, breed etc.), you could also use this setup:

@Test
public void test() throws IOException {
    final Bulldog bulldog = new Bulldog();
    bulldog.setAnimalType("Bulldog");
    // bulldog.setSpecies("Dog");
    // bulldog.setBreed("Bulldog");
    bulldog.setType("B-Dog");

    final ObjectMapper om = new ObjectMapper();
    final String json = om.writeValueAsString(bulldog);
    System.out.println(json);
    final Animal deserialized = om.readValue(json, Animal.class);
    assertTrue(deserialized instanceof Bulldog);

}

@JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
@JsonSubTypes({
        @Type(Dog.class)
})
public static abstract class Animal {

    private String animalType;

    public String getAnimalType() {
        return animalType;
    }

    public void setAnimalType(final String animalType) {
        this.animalType = animalType;
    }
}

@JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
@JsonSubTypes({
        @Type(value = Bulldog.class)
})
public static abstract class Dog
        extends Animal {

}

@JsonTypeName("Bulldog")
public static class Bulldog extends Dog {

    private String type; // "frenchy", "english", etc..

    public String getType() {
        return type;
    }

    public void setType(final String type) {
        this.type = type;
    }
}
like image 98
sfiss Avatar answered Oct 22 '25 01:10

sfiss


I was able to solve this such that the following JSON translates to a Bulldog object:

{
  "species": "Dog",
  "breed": "Bulldog",
  "name": "Sparky",
  "type": "English"
}

I used the following code to do this:

ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
    @Override
    public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, JsonParser p, String msg) throws IOException {
        JsonNode o = p.readValueAsTree();
        JsonNode copy = o.deepCopy();
        JsonNode species = o.get("species");

        if (species != null) {
            Class<? extends Animal> clazz;
            switch (species.asText()) {
                case "Dog":
                    clazz = Dog.class;
                    break;
                case "Cat":
                    clazz = Cat.class;
                    break;
                default:
                    return NOT_HANDLED;
            }

            JsonParser parser = new TreeTraversingParser(copy, p.getCodec());
            parser.nextToken(); // without this an error is thrown about missing "breed" type

            return ctxt.readValue(parser, clazz);
        }

        return NOT_HANDLED;
    }
});

I believe there's probably a better way to find the typed class (I noticed that the there is a cache in one of the inputs to the handleMissingInstantiator method that contains all of the relevant types, that can probably be used to find the type based on name instead of hardcoding values as I'm doing.

like image 31
naydichev Avatar answered Oct 22 '25 01:10

naydichev