基于唯一属性的存在用 Jackson 反序列化多态类型

Posted

技术标签:

【中文标题】基于唯一属性的存在用 Jackson 反序列化多态类型【英文标题】:Deserializing polymorphic types with Jackson based on the presence of a unique property 【发布时间】:2014-08-07 11:18:38 【问题描述】:

如果我有这样的类结构:

public abstract class Parent 
    private Long id;
    ...


public class SubClassA extends Parent 
    private String stringA;
    private Integer intA;
    ...


public class SubClassB extends Parent 
    private String stringB;
    private Integer intB;
    ...

是否有其他方法可以反序列化不同于 @JsonTypeInfo 的方式?在我的父类上使用这个注释:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "objectType")

我宁愿不必强制我的 API 的客户端包含 "objectType": "SubClassA" 以反序列化 Parent 子类。

Jackson 没有使用@JsonTypeInfo,而是提供了一种方法来注释子类并通过唯一属性将其与其他子类区分开来?在我上面的示例中,这类似于“如果 JSON 对象具有 "stringA": ...,则将其反序列化为 SubClassA,如果它具有 "stringB": ...,则将其反序列化为 SubClassB”。

【问题讨论】:

【参考方案1】:

EDIT (2021-07-15) -- 已过时,请参阅 M. Justin's answer 了解当前情况。

(原答案如下)

没有。已经请求了这样一个特性——它可以被称为“类型推断”或“隐含类型”——但还没有人提出一个可行的通用建议来说明它应该如何工作。想办法支持特定案例的特定解决方案很容易,但找出通用解决方案更难。

【讨论】:

并不奇怪,这似乎是杰克逊的指南领域中的一个热门话题。如果我有选择的话,我会避免基于需要反序列化/序列化的域对象的抽象类的模式——对于它可能提供的任何好处来说似乎过于复杂。 @SamB。是的,事情变得相当复杂相当快。并且没有出现任何解决方案也就不足为奇了,而多个开发人员提出了总体思路…… 它已在 Jackson 2.12 中使用 polymorphic type by deduction 实现。我已将其扩展为自己的答案:***.com/a/66167492/1108305【参考方案2】:

此功能已使用“deduction-based polymorphism”添加到 Jackson 2.12。要将其应用于您的案例,只需使用@JsonTypeInfo(use=Id.DEDUCTION) 以及@JsonSubTypes 提供的支持子类型的完整列表即可:

@JsonTypeInfo(use=Id.DEDUCTION)
@JsonSubTypes(@Type(SubClassA.class), @Type(SubClassB.class))
public abstract class Parent 
    private Long id;
    ...

此功能是根据jackson-databind#43 实现的,并在2.12 release notes 中进行了总结:

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

Jackson 创作者写的Jackson 2.12 Most Wanted (1/5): Deduction-Based Polymorphism 文章中给出了稍长的解释。

【讨论】:

【参考方案3】:

处理多态性要么受模型限制,要么需要大量代码和各种自定义反序列化程序。我是JSON Dynamic Deserialization Library 的合著者,它允许独立于模型的 json 反序列化库。 OP问题的解决方案可以在下面找到。请注意,规则以非常简短的方式声明。

public class SOAnswer1 

    @ToString @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static abstract class Parent 
        private Long id;
    

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class SubClassA extends Parent 
        private String stringA;
        private Integer intA;
    

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class SubClassB extends Parent 
        private String stringB;
        private Integer intB;
    

    public static void main(String[] args) 
        String json = "["
                + "    \"id\": 151243,"
                + "    \"stringA\": \"my special string\","
                + "    \"intA\": 1337"
                + ","
                + ""
                + "    \"id\": 734561,"
                + "    \"stringB\": \"use the Force\","
                + "    \"intB\": 451"
                + "]";
        
        // create a deserializer instance
        DynamicObjectDeserializer deserializer = new DynamicObjectDeserializer();
        
        // runtime-configure deserialization rules
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("stringA"),
                DeserializationActionFactory.objectToType(SubClassA.class)));
        
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("stringB"),
                DeserializationActionFactory.objectToType(SubClassB.class)));
        
        List<Parent> deserializedObjects = deserializer.deserializeArray(json, Parent.class);
        
        for (Parent obj : deserializedObjects) 
            System.out.println("Deserialized Class: " + obj.getClass().getSimpleName()+";\t value: "+obj.toString());
        
    

输出:

Deserialized Class: SubClassA;   value: SOAnswer1.SubClassA(super=SOAnswer1.Parent(id=151243), stringA=my special string, intA=1337)
Deserialized Class: SubClassB;   value: SOAnswer1.SubClassB(super=SOAnswer1.Parent(id=734561), stringB=use the Force, intB=451)

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

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

【讨论】:

【参考方案4】:

正如其他人指出的那样,how it should work so it hasn't been implemented 没有达成共识。

如果你有类 Foo、Bar 和它们的父类 FooBar,当你有 JSON 时,解决方案似乎很明显:


  "foo":<value>


  "bar":<value>

但是当你得到时会发生什么并没有共同的答案


  "foo":<value>,
  "bar":<value>

乍一看,最后一个示例似乎是 400 Bad Request 的明显案例,但实际上有许多不同的方法:

    将其作为 400 Bad Request 处理 类型/字段的优先级(例如,如果存在字段错误,则它的优先级高于某些其他字段 foo) 2 的更复杂情况。

我目前适用于大多数情况并尝试尽可能多地利用现有 Jackson 基础架构的解决方案是(每个层次结构只需要 1 个解串器):

public class PresentPropertyPolymorphicDeserializer<T> extends StdDeserializer<T> 

    private final Map<String, Class<?>> propertyNameToType;

    public PresentPropertyPolymorphicDeserializer(Class<T> vc) 
        super(vc);
        this.propertyNameToType = Arrays.stream(vc.getAnnotation(JsonSubTypes.class).value())
                                        .collect(Collectors.toMap(Type::name, Type::value,
                                                                  (a, b) -> a, LinkedHashMap::new)); // LinkedHashMap to support precedence case by definition order
    

    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException 
        ObjectMapper objectMapper = (ObjectMapper) p.getCodec();
        ObjectNode object = objectMapper.readTree(p);
        for (String propertyName : propertyNameToType.keySet()) 
            if (object.has(propertyName)) 
                return deserialize(objectMapper, propertyName, object);
            
        

        throw new IllegalArgumentException("could not infer to which class to deserialize " + object);
    

    @SuppressWarnings("unchecked")
    private T deserialize(ObjectMapper objectMapper,
                          String propertyName,
                          ObjectNode object) throws IOException 
        return (T) objectMapper.treeToValue(object, propertyNameToType.get(propertyName));
    

示例用法:

@JsonSubTypes(
        @JsonSubTypes.Type(value = Foo.class, name = "foo"),
        @JsonSubTypes.Type(value = Bar.class, name = "bar"),
)
interface FooBar 

@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Foo implements FooBar 
    private final String foo;

@AllArgsConstructor(onConstructor_ = @JsonCreator)
@Value
static class Bar implements FooBar 
    private final String bar;

杰克逊配置

SimpleModule module = new SimpleModule();
module.addDeserializer(FooBar.class, new PresentPropertyPolymorphicDeserializer<>(FooBar.class));
objectMapper.registerModule(module);

或者如果您使用的是 Spring Boot:

@JsonComponent
public class FooBarDeserializer extends PresentPropertyPolymorphicDeserializer<FooBar> 

    public FooBarDeserializer() 
        super(FooBar.class);
    

测试:

    @Test
    void shouldDeserializeFoo() throws IOException 
        // given
        var json = "\"foo\":\"foo\"";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    

    @Test
    void shouldDeserializeBar() throws IOException 
        // given
        var json = "\"bar\":\"bar\"";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Bar("bar"));

    

    @Test
    void shouldDeserializeUsingAnnotationDefinitionPrecedenceOrder() throws IOException 
        // given
        var json = "\"bar\":\"\", \"foo\": \"foo\"";

        // when
        var actual = objectMapper.readValue(json, FooBar.class);

        // then
        then(actual).isEqualTo(new Foo("foo"));
    

编辑:我在project 中添加了对此案例的支持以及更多内容。

【讨论】:

想到支持这样的优先顺序真是太酷了。【参考方案5】:

这是我提出的一个解决方案,它在 Erik Gillespie 的基础上有所扩展。它完全符合您的要求,并且对我有用。

使用杰克逊 2.9

@JsonDeserialize(using = CustomDeserializer.class)
public abstract class BaseClass 

    private String commonProp;


// Important to override the base class' usage of CustomDeserializer which produces an infinite loop
@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassA extends BaseClass 
    
    private String classAProp;


@JsonDeserialize(using = JsonDeserializer.None.class)
public class ClassB extends BaseClass 
    
    private String classBProp;


public class CustomDeserializer extends StdDeserializer<BaseClass> 

    protected CustomDeserializer() 
        super(BaseClass.class);
    

    @Override
    public BaseClass deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException 
        TreeNode node = p.readValueAsTree();
        
        // Select the concrete class based on the existence of a property
        if (node.get("classAProp") != null) 
            return p.getCodec().treeToValue(node, ClassA.class);
        
        return p.getCodec().treeToValue(node, ClassB.class);
    


// Example usage
String json = ...
ObjectMapper mapper = ...
BaseClass instance = mapper.readValue(json, BaseClass.class);

如果您想变得更高级,可以扩展CustomDeserializer 以包含一个映射属性名称的Map&lt;String, Class&lt;?&gt;&gt;,当该属性名称存在时,它会映射到特定的类。这种方法在article 中提出。

更新

Jackson 2.12.0 获得 Polymorphic subtype deduction from available fields,其中添加了 @JsonTypeInfo(use = DEDUCTION)

AsDeductionTypeDeserializer 实现从字段中推断出子类型。作为一个不用于合并的 POC,有大量的剪切粘贴代码等,但我认为功能性 PR 将是讨论我出于兴趣而写的内容的最佳基础。

它的工作原理是在注册时对每个子类型的所有可能字段进行指纹识别。在反序列化时,将可用字段与那些指纹进行比较,直到只剩下一个候选者。它专门只查看直接子字段名称,因为现有机制涵盖了直接子值,并且更深入的分析是一项更加艰巨的 ML 任务,而不是 Jackson 职权范围的一部分。

顺便说一句,这里有一个(现已关闭的)Github 问题请求这个:https://github.com/FasterXML/jackson-databind/issues/1627

【讨论】:

在我升级到 Spring Boot 2.5.x 之前,您的原始答案对我来说是一个简单的解决方法,这样我就可以利用 Jackson 2.12 中新的基于演绎的多态性。谢谢。【参考方案6】:

我的应用程序要求我保留旧结构,因此我找到了一种在不更改数据的情况下支持多态性的方法。这是我的工作:

    扩展 JsonDeserializer

    转换成树并读取字段,然后返回子类对象

    @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException 
        JsonNode jsonNode = p.readValueAsTree(); 
        Iterator<Map.Entry<String, JsonNode>> ite = jsonNode.fields();
        boolean isSubclass = false;
        while (ite.hasNext()) 
            Map.Entry<String, JsonNode> entry = ite.next();
            // **Check if it contains field name unique to subclass**
            if (entry.getKey().equalsIgnoreCase("Field-Unique-to-Subclass")) 
                isSubclass = true;
                break;
            
        
        if (isSubclass) 
            return mapper.treeToValue(jsonNode, SubClass.class);
         else 
            // process other classes
        
    
    

【讨论】:

【参考方案7】:

这感觉像是应该使用 @JsonTypeInfo@JsonSubTypes 的东西,但我已经通过文档进行了挑选,并且可以提供的所有属性似乎都与您所描述的不符。

您可以编写一个自定义反序列化器,以非标准方式使用@JsonSubTypes'“name”和“value”属性来完成您想要的。反序列化器和@JsonSubTypes 将在您的基类上提供,反序列化器将使用“名称”值来检查属性是否存在,如果存在,则将 JSON 反序列化为“值”属性中提供的类.您的课程将如下所示:

@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes(
        @Type(name = "stringA", value = SubClassA.class),
        @Type(name = "stringB", value = SubClassB.class)
)
public abstract class Parent 
    private Long id;
    ...


public class SubClassA extends Parent 
    private String stringA;
    private Integer intA;
    ...


public class SubClassB extends Parent 
    private String stringB;
    private Integer intB;
    ...

【讨论】:

PropertyPresentDeserializer 似乎是个好东西。但是,它似乎没有包含在杰克逊中。查看 GitHub 搜索结果:github.com/… @koppor 这是我推荐 OP 创建的自定义反序列化器的占位符名称。

以上是关于基于唯一属性的存在用 Jackson 反序列化多态类型的主要内容,如果未能解决你的问题,请参考以下文章

Jackson多态反序列化的使用

jackson-- JsonTypeInfo多态反序列化

json之jackson序列化反序列化探究(二)

JSON Jackson - 使用自定义序列化程序序列化多态类时的异常

如何根据json中的属性编写jackson反序列化器

用Jackson进行Json序列化时的常用注解