杰克逊的@JsonSubTypes对于多态反序列化仍然必要吗?

64
我可以对一个类层次结构进行序列化和反序列化,其中抽象基类带有注释。
@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")

但是没有列出子类的@JsonSubTypes,而且子类本身也没有太多的注释,只有构造函数上的@JsonCreator。 ObjectMapper是普通的,我没有使用mixin。

Jackson文档关于多态反序列化和“类型标识符”建议(强烈)在抽象基类上使用@JsonSubTypes注解,或者在mixin上使用它,或者需要向ObjectMapper注册子类型。 有很多SO问题和/或博客文章都同意。 然而它确实有效。(这是Jackson 2.6.0。

所以,我是受益于尚未记录的功能还是依赖于未记录的行为(可能会改变),还是其他原因?(我问这个问题是因为我真的不想是后两者之一。但是我必须知道。)
编辑:添加代码 - 还有一个注释。注释是:我应该提到,我正在反序列化的所有子类都位于与基本抽象类相同的包和同一个jar文件中。
抽象基类:
package so;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")
public abstract class PolyBase
{
    public PolyBase() { }

    @Override
    public abstract boolean equals(Object obj);
}

它的一个子类:
package so;
import org.apache.commons.lang3.builder.EqualsBuilder;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public final class SubA extends PolyBase
{
    private final int a;

    @JsonCreator
    public SubA(@JsonProperty("a") int a) { this.a = a; }

    public int getA() { return a; }

    @Override
    public boolean equals(Object obj) {
        if (null == obj) return false;
        if (this == obj) return true;
        if (this.getClass() != obj.getClass()) return false;

        SubA rhs = (SubA) obj;
        return new EqualsBuilder().append(this.a, rhs.a).isEquals();
    }
}

子类SubBSubC是相同的,除了在SubB中字段a声明为String(而不是int),在SubC中声明为boolean(而不是int)(并相应地修改getA方法)。

测试类:

package so;    
import java.io.IOException;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.*;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestPoly
{
    public static class TestClass
    {
        public PolyBase pb1, pb2, pb3;

        @JsonCreator
        public TestClass(@JsonProperty("pb1") PolyBase pb1,
                         @JsonProperty("pb2") PolyBase pb2,
                         @JsonProperty("pb3") PolyBase pb3)
        {
            this.pb1 = pb1;
            this.pb2 = pb2;
            this.pb3 = pb3;
        }

        @Override
        public boolean equals(Object obj) {
            if (null == obj) return false;
            if (this == obj) return true;
            if (this.getClass() != obj.getClass()) return false;

            TestClass rhs = (TestClass) obj;
            return new EqualsBuilder().append(pb1, rhs.pb1)
                                      .append(pb2, rhs.pb2)
                                      .append(pb3, rhs.pb3)
                                      .isEquals();
        }
    }

    @Test
    public void jackson_should_or_should_not_deserialize_without_JsonSubTypes() {

        // Arrange
        PolyBase pb1 = new SubA(5), pb2 = new SubB("foobar"), pb3 = new SubC(true);
        TestClass sut = new TestClass(pb1, pb2, pb3);

        ObjectMapper mapper = new ObjectMapper();

        // Act
        String actual1 = null;
        TestClass actual2 = null;

        try {
            actual1 = mapper.writeValueAsString(sut);
        } catch (IOException e) {
            fail("didn't serialize", e);
        }

        try {
            actual2 = mapper.readValue(actual1, TestClass.class);
        } catch (IOException e) {
            fail("didn't deserialize", e);
        }

        // Assert
        assertThat(actual2).isEqualTo(sut);
    }
}

这个测试通过了。如果你在第二个try {行出现错误,你可以检查actual1,看一下结果:
{"pb1":{"@class":".SubA","a":5},
 "pb2":{"@class":".SubB","a":"foobar"},
 "pb3":{"@class":".SubC","a":true}}

所以这三个子类被正确地序列化(每个子类都有其类名作为id),然后反序列化,结果相等(每个子类都有一个“值类型”的equals())。
1个回答

81

在使用Jackson进行序列化和反序列化时,有两种实现多态的方式。它们被定义在您发布的链接第1节用法中。

你的代码

@JsonTypeInfo(
    use = JsonTypeInfo.Id.MINIMAL_CLASS,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@class")

这是第二种方法的示例。首先要注意的是,所有注解类型及其子类型的实例都使用这些设置(除非被另一个注解覆盖)。

 

因此,此配置值传播到所有子类型。然后,我们需要一个类型标识符,将Java类型映射到JSON字符串中的文本值,反之亦然。在您的示例中,这由JsonTypeInfo.Id#MINIMAL_CLASS给出。

 

意味着使用最小路径的Java类名作为类型标识符。

因此,在序列化时,从目标实例生成最小类名并写入JSON内容; 或者在反序列化时使用最小类名来确定目标类型。

您也可以使用JsonTypeInfo.Id#NAME,它

 

表示使用逻辑类型名称作为类型信息; 然后需要单独解析名称以获取实际的具体类型(Class)。

为提供这样的逻辑类型名称,您可以使用@JsonSubTypes

 

JsonTypeInfo 一起使用的注释,用于指示可序列化的多态类型的子类型,并关联用于JSON内容中的逻辑名称(这比使用物理Java类名称更可移植)。

这只是实现相同结果的另一种方法。您所问的文档说明如下:

 

基于Java类名的类型ID相当简单:只是类名,可能会有一些简单的前缀删除(对于“最小”变体)。但是类型名称不同:必须在逻辑名称和实际类之间建立映射。

因此,处理类名的各种 JsonTypeInfo.Id 值都很简单,因为它们可以自动生成。但是,对于类型名称,您需要显式地给出映射值。


2
感谢您解释了@JsonSubTypes仅在使用NAME表单时才需要。我读了那么多文档,却没有想到这一点... - davidbak
8
这里的问题是,如果你使用名称表单(这是极为理想的),你就无法动态添加类或在以后添加类。 - Dave
4
在个别具体类上使用JsonTypeInfo不更合理吗?在接口上使用似乎违反了开闭原则(也就是说,我们需要在每次添加具体实现时修改接口)。如果我理解有误,请纠正我。 - Harshit
1
@HDave:你可以使用ObjectMapper#registerSubtypes,可能需要结合Reflections等工具。 - chrylis -cautiouslyoptimistic-
2
正如c-c所提到的,我们成功地使用了@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, property="message_type"),然后将每个类都注册到新创建的映射器中:mapper.registerSubtypes(new NamedType(SomeClass.class, "some_class"));。当您在运行时知道所有类但无法更改基类时,这种方法是可行的,因此您不需要在JsonSubTypes中列出所有内容。 - Wheezil
2
这里展示了前两个评论所讨论的内容的示例:https://dev59.com/QrPma4cB1Zd3GeqPsItv#56020646。 - jaco0646

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