Полиморфизм Джексона: вложенные подтипы - PullRequest
1 голос
/ 04 февраля 2020

Можно ли использовать несколько @JsonSubType аннотаций во вложенном виде?

Например, представьте следующие классы:

@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..
}

Если я использую объектное сопоставление, я могу успешно сопоставить Bulldog с JSON, однако, при попытке прочитать результирующий JSON и прочитать его обратно, я получаю ошибку, подобную следующей:

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

Возможно ли заставить Джексона работать с таким подтипом? Нужно ли создавать собственный десериализатор для каждого подкласса?

EDIT :

Я немного изменил приведенные выше классы по сравнению с исходной публикацией. Я добавил класс Cat и получил его и Dog от Animal.

Вот пример JSON, который можно создать с помощью ObjectMapper::writeValueAsString:

{
  "name": null,
  "species": "Dog",
  "breed": "Bulldog",
  "type": "B-Dog"
}

Ответы [ 2 ]

1 голос
/ 04 февраля 2020

Мне удалось решить эту проблему так, что следующее JSON преобразуется в Bulldog объект:

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

Я использовал следующий код для этого:

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;
    }
});

Я полагаю, что, возможно, есть лучший способ найти типизированный класс (я заметил, что в одном из входных данных метода handleMissingInstantiator, который содержит все соответствующие типы, можно использовать cache, который, вероятно, можно использовать для поиска тип, основанный на имени, а не на жестких значениях, как я.

1 голос
/ 04 февраля 2020

Следующее работает, если я использую @JsonTypeInfo и аналогичную настройку для вас. Возможно, ваша проблема в коде десериализации, поэтому взгляните на это:

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;
        }
    }
}

РЕДАКТИРОВАНО для обновленного вопроса: если вы можете использовать то же свойство (в следующем коде скрытое свойство "@class" ) для иерархии наследования это работает:

    @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 {

}

Если вы хотите установить тип животного (например, для вычисления вида, породы и т. д. c.), вы также можете использовать эту настройку:

@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;
    }
}
...