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

62
如果我有这样的一个类结构:
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的客户端在反序列化Parent子类时必须包含"objectType": "SubClassA"

除了使用@JsonTypeInfo之外,Jackson是否提供了一种通过唯一属性注释子类并将其与其他子类区分开的方法?在上面的例子中,这可能是这样的:如果一个JSON对象具有"stringA": ...,则将其反序列化为SubClassA,如果它具有"stringB": ...,则将其反序列化为SubClassB。"

8个回答

56

这是我想出的一个解决方案,对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<?>>,该映射将属性名称(当存在时)映射到特定的类。这种方法在此 文章中介绍。

更新

Jackson 2.12.0 引入了从可用字段推断多态子类型的功能,具体内容请参见此处,其中添加了@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中的新的基于推断的多态性。谢谢。 - flaviuratiu
谢谢这个。在我看来,这应该是被接受的答案。 - undefined

30

我感觉应该使用@JsonTypeInfo@JsonSubTypes,但是我阅读了文档,提供的属性似乎都不太适用于你所描述的情况。

你可以编写一个自定义反序列化器,以非标准的方式使用@JsonSubTypes中的"name"和"value"属性来实现你想要的目标。反序列化器和@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
PropertyPresentDeserializer 看起来是一件好事。然而,它似乎并未包含在 Jackson 中。请查看 GitHub 搜索结果:https://github.com/search?utf8=%E2%9C%93&q=PropertyPresentDeserializer+in%3Apath%2Cfile&type=Code - koppor
@koppor是我建议原帖作者创建的自定义反序列化器的占位符名称。 - Erik Gillespie

16

正如其他人指出的那样,关于它应该如何工作,因此尚未实施,没有共识。

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

{
  "foo":<value>
}

或者

{
  "bar":<value>
}

但是当你遇到{{问题}}时,没有通用的解决方案。

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

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

  1. 将其处理为400 Bad Request
  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;
}

杰克逊配置
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"));
    }

编辑:我已经在这个项目中添加了对此情况和更多情况的支持。


想到支持这样的优先顺序真是太酷了。 - Sam Berry

11

编辑(2021-07-15)-- 已过时,请参阅M. Justin的答案了解当前情况。

(以下为原始回答)

没有。虽然有人提出过这样一个功能,可以称之为“类型推断”或“隐式类型”,但到目前为止,还没有人提出可行的通用方案来实现它。支持特定情况下的特定解决方案很容易想到,但找出通用解决方案更加困难。


不足为奇,这似乎是杰克逊的教程领域中比较流行的话题。如果可以选择,我会避免基于抽象类的模式来处理需要进行序列化/反序列化的领域对象--这似乎过于复杂,而且可能带来的好处也不明显。 - Sam Berry
@SamB。没错,事情很快变得非常复杂;并且不足为奇的是,虽然多个开发人员提出了一般性的想法,但没有出现解决方案... - StaxMan
2
它已经在Jackson 2.12中使用通过推断的多态类型实现。我已经将其扩展为自己的答案:https://dev59.com/I2Af5IYBdhLWcg3wylQd#66167492 - M. Justin

10

使用“基于推断的多态性”,Jackson 2.12增加了此功能。要将其应用于您的情况,只需在@JsonSubTypes提供的完整子类型列表旁边使用@JsonTypeInfo(use=Id.DEDUCTION)即可:

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

此功能是根据 jackson-databind#43 实现的,并在 2.12 发布说明 中进行了总结:
基本上它允许省略实际的 Type Id 字段或值,只要可以从字段的存在中推断出子类型 (@JsonTypeInfo(use=DEDUCTION))。也就是说,每个子类型都有一个不同的包含字段集,因此在反序列化期间可以唯一且可靠地检测到类型。
Jackson 的创建者在 Jackson 2.12 Most Wanted (1/5): Deduction-Based Polymorphism 文章中给出了稍长一些的解释。

6

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

  1. Extend JsonDeserializer
  2. Convert to Tree and read field, then return Subclass object

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

1

看到上面有几个答案,但我想加上我的意见,因为这在过去的两年中用于生产。

我们有车辆的速度和位置的事件类型。
使用@JsonTypeInfo 可以实现动态绑定。

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "eventId")
@JsonSubTypes({
        @JsonSubTypes.Type(value = SpeedEvent.class, name = "10"),
        @JsonSubTypes.Type(value = LocationEvent.class, name = "11")
})
public interface EventDetails {
}

public class Event {

    private int eventId;
    private long ts;
    private EventDetails eventDetails;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public long getTs() {
        return ts;
    }

    public void setTs(long ts) {
        this.ts = ts;
    }

    public EventDetails getEventDetails() {
        return eventDetails;
    }

    public void setEventDetails(EventDetails eventDetails) {
        this.eventDetails = eventDetails;
    }
}

public class SpeedEvent implements EventDetails {

    private int eventId;
    private int speed;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public int getSpeed() {
        return speed;
    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }
}

public class LocationEvent implements EventDetails {

    private int eventId;
    private double latitude;
    private double longitude;

    public int getEventId() {
        return eventId;
    }

    public void setEventId(int eventId) {
        this.eventId = eventId;
    }

    public double getLatitude() {
        return latitude;
    }

    public void setLatitude(double latitude) {
        this.latitude = latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public void setLongitude(double longitude) {
        this.longitude = longitude;
    }
}

示例输出:-
速度事件

{
    "eventId": 10,
    "ts": 1684937053366,
    "eventDetails": {
        "eventId": "10",
        "speed": 50
    }
}

位置事件

{
    "eventId": 11,
    "ts": 1684937172450,
    "eventDetails": {
        "eventId": "11",
        "latitude": 41.40338,
        "longitude": 2.17403
    }
}

0

处理多态性要么是模型绑定的,要么需要大量使用各种自定义反序列化器的代码。我是JSON动态反序列化库的共同作者,该库允许进行独立于模型的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)

Maven 依赖项 pretius-jddl(请查看最新版本:maven.org/jddl):

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接