如果存在特定字段,是否可以使用 Jackson 多态反序列化序列化为子类型?

在动物园上使用旋转示例:

public class ZooPen {
    public String type;
    public List<Animal> animals;
}

public class Animal {
    public String name;
    public int age;
}

public class Bird extends Animal {
    public double wingspan;
}

我想使用多态反序列化来构造实例(如果未指定翼展),如果指定翼展,则使用实例。在 Jackson 中,非类型化反序列化通常如下所示:AnimalBird

@JsonTypeInfo( 
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "wingspan",
    visible = true,
    defaultImpl = Animal.class
)
@JsonSubTypes({
    @Type(value = Bird.class, name = "bird")
})  
public class Animal {
    ...
}

翼展值可以是任何内容,如果没有匹配某些特定值,Jackson 将回退到默认的Impl类。

我可能会使用:@JsonCreator

@JsonCreator
public static Animal create(Map<String,Object> jsonMap) 
        throws JsonProcessingException {

    ObjectMapper mapper = new ObjectMapper();
    if (jsonMap.get("wingspan") == null) {
        // Construct and return animal
    } else {
        // Construct and return bird
    }
}

但是,我必须手动处理额外的值并引发一致的异常,并且不清楚以后是否会正确序列化。Animal

似乎我可以使用自己的或TypeIdResolver,但这似乎比我自己反序列化原始json要多得多。此外,似乎本质上假设类型信息是序列化的,所以这些都不好用。TypeResolverTypeResolverTypeIdResolver

实现我自己的JsonDeserializer是否可行,该JsonDeserializer挂接到生命周期中以指定类型,但仍使用基本的Jackson注释处理功能?我一直在看JsonDeserializer.deserializeWithType(...),但这似乎将反序列化完全委托给.还有一个问题是,在我知道要使用哪种类型之前,我需要反序列化一些对象。TypeDeserializer

或者,可能有一种方法可以定位动物园围栏的类型,即使它位于父对象中。

有没有办法通过多态类型处理来做我想做的事情?


答案 1

从 Jackson 2.12.2 开始,以下内容使用“基于演绎的多态性”功能实现目标。如果存在与子类型(即 )不同的属性,则反序列化类型将为 ;否则它将是:BirdwingspanBirdAnimal

@JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = Animal.class)
@JsonSubTypes({@Type(Bird.class)})
public class Animal {
    public String name;
    public int age;
}

基于演绎的多态性

基于演绎的多态性特征根据存在与特定子类型不同的属性来推断子类型。如果没有可由子类型特定属性唯一标识的子类型,则将使用由值指定的类型。defaultImpl

基于演绎的多态性功能在 Jackson 2.12 中根据 jackson-databind#43 实现,并在 2.12 发行说明中进行了总结:

它基本上允许省略实际的 Type Id 字段或值,只要子类型可以从字段的存在中推导出 ()。也就是说,每个子类型都有一组不同的字段,因此在反序列化期间可以唯一可靠地检测类型。@JsonTypeInfo(use=DEDUCTION)

当没有唯一可识别的子类型时,这种指定默认类型(而不是引发异常)的功能是由 Jackson-databind#3055 在 Jackson 2.12.2 中添加的:

在没有单个候选者的情况下,无论适用性如何,都应是目标类型。defaultImpl

杰克逊2.12最想要的(1/5):杰克逊创作者撰写的基于演绎的多态性文章中给出了对基于演绎的多态性的稍长解释。


答案 2

编辑:如果您可以使用最新的 Jackson 候选版本,您的问题就解决了。我在这里组装了一个快速演示 https://github.com/MariusSchmidt/de.denktmit.stackoverflow/tree/main/de.denktmit.jackson

您应该 https://github.com/FasterXML/jackson-databind/issues/1627 查看此线程,因为它讨论了您的问题并提出了解决方案。有一个合并,对我来说看起来很有希望 https://github.com/FasterXML/jackson-databind/pull/2813。因此,您可以尝试遵循@JsonTypeInfo(使用=推断)的路径。

但是,如果您无法使用最新即将推出的杰克逊版本,那么我可能会这样做:

向后移植合并请求,或

  1. 使用 Jackson 将输入反序列化为通用 JsonNode
  2. 使用 https://github.com/json-path/JsonPath 检查是否存在一个或多个属性。某些容器类可以包装唯一标识类类型所需的所有路径。
  3. 将 JsonNode 映射到确定的类,如此处所述 将 JsonNode 转换为 POJO

通过这种方式,您可以充分利用 Jackson 的全部功能,而无需处理低级映射逻辑。

此致敬意

马吕斯

动物

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird;
import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish;
import org.junit.jupiter.api.Test;

import java.util.List;

import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id.DEDUCTION;
import static org.assertj.core.api.Assertions.assertThat;

@JsonTypeInfo(use = DEDUCTION)
@JsonSubTypes( {@JsonSubTypes.Type(Bird.class), @JsonSubTypes.Type(Fish.class)})
public class Animal {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class Bird extends de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal {
    private double wingspan;

    public double getWingspan() {
        return wingspan;
    }

    public void setWingspan(double wingspan) {
        this.wingspan = wingspan;
    }
}

public class Fish extends de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal {

    private boolean freshwater;

    public boolean isFreshwater() {
        return freshwater;
    }

    public void setFreshwater(boolean freshwater) {
        this.freshwater = freshwater;
    }
}

动物园围栏

public class ZooPen {

    private String type;
    private List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> animals;

    public String getType() {
        return type;
    }

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

    public List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> getAnimals() {
        return animals;
    }

    public void setAnimals(List<de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal> animals) {
        this.animals = animals;
    }
}

测试

import com.fasterxml.jackson.databind.ObjectMapper;
        import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal;
        import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird;
        import de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish;
        import de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen;
        import org.junit.jupiter.api.Test;

        import static org.assertj.core.api.Assertions.assertThat;

public class DeductivePolymorphicDeserializationTest {

    private static final String birdString = "{\n" +
            "      \"name\": \"Tweety\",\n" +
            "      \"age\": 79,\n" +
            "      \"wingspan\": 2.9\n" +
            "    }";

    private static final String fishString = "{\n" +
            "      \"name\": \"Nemo\",\n" +
            "      \"age\": 16,\n" +
            "      \"freshwater\": false\n" +
            "    }";

    private static final String zooPenString = "{\n" +
            "  \"type\": \"aquaviary\",\n" +
            "  \"animals\": [\n" +
            "    {\n" +
            "      \"name\": \"Tweety\",\n" +
            "      \"age\": 79,\n" +
            "      \"wingspan\": 2.9\n" +
            "    },\n" +
            "    {\n" +
            "      \"name\": \"Nemo\",\n" +
            "      \"age\": 16,\n" +
            "      \"freshwater\": false\n" +
            "    }\n" +
            "  ]\n" +
            "}";
    private final ObjectMapper mapper = new ObjectMapper();

    @Test
    void deserializeBird() throws Exception {
        de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal animal = mapper.readValue(birdString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal.class);
        assertThat(animal).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.Bird.class);
    }

    @Test
    void deserializeFish() throws Exception {
        de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal animal = mapper.readValue(fishString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.Animal.class);
        assertThat(animal).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.Fish.class);
    }

    @Test
    void deserialize() throws Exception {
        de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen zooPen = mapper.readValue(zooPenString, de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen.class);
        assertThat(zooPen).isInstanceOf(de.denktmit.stackoverflow.jackson.polymorphic.deductive.ZooPen.class);
    }
}