用Jackson反序列化多态类型

时间:2014-06-17 12:31:58

标签: json jackson deserialization

如果我有类似的类结构:

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子类。

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

7 个答案:

答案 0 :(得分:13)

这感觉就像应该使用@JsonTypeInfo@JsonSubTypes这样的内容,但是我已经通过文档进行了选择,并且所提供的任何属性都不会与您所提供的内容相匹配。重新描述。

您可以编写一个使用@JsonSubTypes'的自定义反序列化程序。 "名称"和"价值"属性以非标准的方式来实现你想要的。反序列化器和@JsonSubTypes将在您的基类上提供,反序列化器将使用" name"检查属性是否存在的值以及是否存在,然后将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;
    ...
}

答案 1 :(得分:9)

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

使用Jackson 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<String, Class<?>>,该{{1}}映射属性名称,该名称在存在时映射到特定的类。这种方法在article中提出。

顺便说一下,这里有一个github问题:https://github.com/FasterXML/jackson-databind/issues/1627

答案 2 :(得分:7)

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

答案 3 :(得分:3)

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

  1. 扩展JsonDeserializer
  2. 转换为Tree并读取字段,然后返回Subclass对象

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

答案 4 :(得分:2)

正如其他人所指出的,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的例子,但实际上有许多不同的方法:

  1. 将其作为400错误请求处理
  2. 按类型/字段的优先级(例如,如果存在字段错误,则其优先级高于其他某些字段foo)
  3. 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;
}

Jackson配置

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

答案 5 :(得分:1)

此功能已使用“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 2.12 Most Wanted (1/5): Deduction-Based Polymorphism 文章中给出了稍长的解释。

答案 6 :(得分:0)

处理多态要么是模型绑定的,要么需要大量带有各种自定义反序列化器的代码。我是 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>