杰克逊多态反序列化 - 您是否可以要求存在字段而不是特定值?

时间:2013-05-10 18:50:55

标签: java json polymorphism jackson deserialization

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

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

如果没有指定翼展,我想使用多态反序列化来构造Animal实例,如果是,则使用Bird实例。在Jackson中,无类型反序列化通常看起来像这样:

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

翼展值可以是任何东西,如果没有特定匹配的东西,杰克逊会回到defaultImpl类。

我可以使用@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是否会在以后正确序列化。

有没有办法用多态类型处理做我想做的事情?我似乎可以使用自己的TypeResolver或TypeIdResolver,但这似乎比仅仅反序列化原始json更多的工作。或者,可能有一种方法可以定位动物园笔的类型,即使它位于父对象中。有什么想法吗?

修改
TypeResolver和TypeIdResolver似乎本质上假设类型信息是序列化的,所以这些不好用。是否可以实现我自己的JsonDeserializer,它挂钩到生命周期来指定类型,但仍然使用基本的jackson注释处理功能?我一直在查看JsonDeserializer.deserializeWithType(...),但这似乎完全将反序列化委托给TypeDeserializer。在我知道要使用哪种类型之前,我还需要对某些对象进行反序列化。

6 个答案:

答案 0 :(得分:2)

Jackson 2.12 添加了“deduction-based polymorphism”以根据与特定子类型不同的属性的存在自动推断子类型。从 Jackson 2.12.2 开始,当没有子类型特定属性唯一可识别的子类型时,可以指定要使用的类型。

这些功能可用于在 Jackson 2.12.2 或更高版本中完成请求的反序列化。为此,请在 @JsonTypeInfo(use=Id.DEDUCTION, defaultImpl = Animal.class) 提供的受支持子类型的完整列表旁边使用 @JsonSubTypes

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

基于推论的多态性

基于推导的多态性功能是按照 jackson-databind#43 实现的,并在 2.12 release notes 中进行了总结:

<块引用>

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

此行为由 Jackson 2.12.2 中的 jackson-databind#3055 改进:

<块引用>

在没有一个候选人的情况下,无论是否合适,defaultImpl 都应该是目标类型。

Jackson 创建者撰写的 Jackson 2.12 Most Wanted (1/5): Deduction-Based Polymorphism 文章中对基于演绎的多态性进行了稍长的解释。

答案 1 :(得分:0)

如果你没与杰克逊结婚,我相信使用FlexJSON可以实现与此类似的东西。

http://flexjson.sourceforge.net/javadoc/flexjson/JSONDeserializer.html

我不熟悉Jackson执行类似操作的方法,但我可以说FlexJSON非常高效,并且在序列化/反序列化步骤中通常很直观。

答案 2 :(得分:0)

虽然没有直接回答您的问题,但我确实有必要指出,使用@JsonCreator并不太麻烦:

@JsonCreator
public static Animal create(Map<String,Object> jsonMap) {
    String name = (String) jsonMap.get("name");
    int age = (int) jsonMap.get("age");
    if (jsonMap.keySet().contains("wingspan")) {
        double wingspan = (double) jsonMap.get("wingspan");
        return new Bird(name, age, wingspan);
    } else {
        return new Animal(name, age);
    }
}

无需抛出JsonProcessingException。此自定义解串器将由于与内置Jackson解串器完全相同的原因而失败,即强制转换异常。对于复杂的反序列化,我更喜欢这种处理方式,因为它使代码更易于理解和修改。

答案 3 :(得分:0)

编辑:如果您可以使用最新的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(use = DEDUCTION)的路径。

但是,如果您不能使用即将推出的最新Jackson版本,这是我可能会做的事情:

向后移植合并请求,或者

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

这样,您可以利用Jackson的全部功能,而无需处理低级映射逻辑

最诚挚的问候,

Marius

动物

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

ZooPen

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

答案 4 :(得分:0)

您可以使用 pretius-jddl 反序列化来实现您的目标。我扩展了类层次结构来展示它是如何工作的。这是一个示例代码:

public class SOAnswer3 {

    @ToString @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Animal {
        String name;
        int age;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Bird extends Animal {
        double wingspan;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Elephant extends Animal {
        double trunkLength;
    }

    public static void main(String[] args) {
        String json = "[{"
                + "    \"name\": \"Marty\","
                + "    \"age\": 3"
                + "},"
                + "{"
                + "    \"name\": \"Danny\","
                + "    \"age\": 7,"
                + "    \"wingspan\": 1.4159"
                + "},{"
                + "    \"name\": \"King\","
                + "    \"age\": 21,"
                + "    \"trunkLength\": 2.11"
                + "}]";
        
        // create a deserializer instance
        DynamicObjectDeserializer deserializer = new DynamicObjectDeserializer();
        
        // runtime-configure deserialization rules
        deserializer.addRule(DeserializationRuleFactory.newRule(1, // priority 
                DeserializationCriterionFactory.targetClassIsAssignableFrom(Animal.class)
                    .and((e) -> e.getJsonNode().has("wingspan")),
                DeserializationActionFactory.objectToType(Bird.class)));
        
        deserializer.addRule(DeserializationRuleFactory.newRule(1,
                DeserializationCriterionFactory.targetClassIsAssignableFrom(Animal.class)
                    .and((e) -> e.getJsonNode().has("trunkLength")),
                DeserializationActionFactory.objectToType(Elephant.class)));
        
        List<Animal> deserializedObjects = deserializer.deserializeArray(json, Animal.class);
        
        for (Animal obj : deserializedObjects) {
            System.out.println("Deserialized Class: " + obj.getClass().getSimpleName()+";\t value: "+obj.toString());
        }
    }
}

结果:

Deserialized Class: Animal;  value: SOAnswer3.Animal(name=Marty, age=3)
Deserialized Class: Bird;    value: SOAnswer3.Bird(super=SOAnswer3.Animal(name=Danny, age=7), wingspan=1.4159)
Deserialized Class: Elephant;    value: SOAnswer3.Elephant(super=SOAnswer3.Animal(name=King, age=21), trunkLength=2.11)

pretius-jddl 的 Maven 依赖(在 maven.org/jddl 查看最新版本:

<dependency>
  <groupId>com.pretius</groupId>
  <artifactId>jddl</artifactId>
  <version>1.0.0</version>
</dependency>

答案 5 :(得分:-1)

您好,Shaun,使用Jackson继承,您可以很容易地实现这种行为。我在这里为动物和鸟类场景建模。

Imp 中的构造函数允许实例​​化Animal的正确实例(即,如果存在名称和年龄,则为Animal;如果存在年龄和翼展名称,则为Bird)。使用泽西(Jersey)之类的方法通过API检索值的工作原理相同

x = ['CPC', 'AID CHECKS']
counts = [208, 28]
print (x)
print (counts)

source = ColumnDataSource(data=dict(x=x, counts=counts))

p = figure(x_range=FactorRange(*x), plot_height=600, plot_width=990, title="NPS Locations by Security Checks")

p.xaxis.axis_label_text_font_size = "5pt"
p.xaxis.axis_label_text_font_style='bold'

p.vbar(x='x', top='counts', width=0.9, source=source)

p.add_tools(HoverTool(tooltips=[("LOCATION", "@x"), ("TOTAL", "@counts")]))


p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None

show(p)