杰克逊反序列化抽象类的子类对象列表

时间:2017-07-17 19:46:15

标签: java json jackson deserialization

我正在尝试设置一个基于REST的Spring Boot应用程序,我有一个接受SubscriptionProduct对象的POST请求。 SubscriptionProduct类有一个Services列表,其中Services是一个带有两个子类的抽象类; OnDemandService和LiveService。应用程序无法反序列化传入的JSON对象。

抽象基类已经注释了我认为必要的杰克逊注释。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = false)
@JsonSubTypes({
        @JsonSubTypes.Type(value = LiveLinearServiceView.class, name = "LIVE"),
        @JsonSubTypes.Type(value = OnDemandServiceView.class, name = "ONDEMAND")
})

当我尝试运行反序列化时,收到以下错误消息:

13:34:09.000 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: context [DefaultTestContext@7f3b84b8 testClass = SerializationTest, testInstance = no.acme.SerializationTest@1b7cc17c, testMethod = testSerializeAndDeserializeSubscriptionProduct@SerializationTest, testException = com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of no.acme.domain.service.Service: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: {
  "type" : "SUBSCRIPTION",
  "id" : null,
  "type" : "SUBSCRIPTION",
  "name" : "Test Product",
  "services" : [ {
    "type" : "LIVE",
    "id" : null,
    "type" : "LIVE",
    "name" : "Test Service",
    "logoURL" : "http://localhost:8080/test.png"
  } ]
}; line: 6, column: 18] (through reference chain: no.acme.dto.product.SubscriptionProductView["services"]->java.util.ArrayList[0]), mergedContextConfiguration = [MergedContextConfiguration@57a3af25 testClass = SerializationTest, locations = '{}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3901d134, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@794cb805], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]], class annotated with @DirtiesContext [false] with mode [null], method annotated with @DirtiesContext [false] with mode [null].

com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of no.acme.domain.service.Service: abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: {
  "type" : "SUBSCRIPTION",
  "id" : null,
  "type" : "SUBSCRIPTION",
  "name" : "Test Product",
  "services" : [ {
    "type" : "LIVE",
    "id" : null,
    "type" : "LIVE",
    "name" : "Test Service",
    "logoURL" : "http://localhost:8080/test.png"
  } ]
}; line: 6, column: 18] (through reference chain: no.acme.dto.product.SubscriptionProductView["services"]->java.util.ArrayList[0])

    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
    at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1456)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1012)
    at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:149)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:287)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:259)
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
    at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:504)
    at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:104)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:276)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:178)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:150)
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:129)
    at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeWithType(BeanDeserializerBase.java:1082)
    at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:63)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3798)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2842)
    at no.acme.SerializationTest.testSerializeAndDeserializeSubscriptionProduct(SerializationTest.java:57)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

我创建了一个小测试项目spring-boot-deserialization-issue,其中包含一个测试用例,显示反序列化问题。

测试用例

@Test
public void testSerializeAndDeserializeSubscriptionProduct() throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
    mapper.registerModule(new JavaTimeModule());

    LiveService service = new LiveService("Test Service", new URL("http://localhost:8080/test.png"));
    List<Service> services = new ArrayList<>();
    services.add(service);
    SubscriptionProduct product = new SubscriptionProduct("Test Product");
    product.setServices(services);

    SubscriptionProductView productView = new SubscriptionProductView(product);

    String jsonString = mapper.writeValueAsString(productView);
    log.info(jsonString);

    SubscriptionProductView mappedProductView = mapper.readValue(jsonString, SubscriptionProductView.class);
    Assert.assertEquals("Test Product", mappedProductView.name);
    Assert.assertEquals(1, mappedProductView.services.size());
}

运行测试

  

mvn test

SubscriptionProductView类 这是发布到REST API的对象,REST API具有服务对象列表。

public class SubscriptionProductView extends ProductView {

    public List<ServiceView> services;

    protected SubscriptionProductView() {
        super();
    }

    public SubscriptionProductView(SubscriptionProduct product) {
        super(product);
        this.setServices(product.getServices());
    }

    public void setServices(List<Service> services) {
        this.services = new ArrayList<>();
        for (Service service : services) {
            switch (service.getType()) {
                case ONDEMAND:
                    this.services.add(new OnDemandServiceView((OnDemandService)service));
                    break;
                case LIVE:
                    this.services.add(new LiveLinearServiceView((LiveService)service));
                    break;
                default:
                    throw new IllegalArgumentException("Unknown service type: " + service.getType());
            }
        }
    }
}

ServiceView类 抽象基类已经注释了我认为必要的杰克逊注释。

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = false)
@JsonSubTypes({
        @JsonSubTypes.Type(value = LiveLinearServiceView.class, name = "LIVE"),
        @JsonSubTypes.Type(value = OnDemandServiceView.class, name = "ONDEMAND")
})
public abstract class ServiceView {

    public Long id;

    @Enumerated(EnumType.STRING)
    public ServiceType type;

    public String name;

    protected ServiceView() {
    }

    @JsonCreator
    public ServiceView(@JsonProperty("id")Long id, @JsonProperty("type")ServiceType type, @JsonProperty("name")String name) {
        this.id = id;
        this.type = type;
        this.name = name;
    }

    public ServiceView(Service service) {
        this.id = service.getId();
        this.type = service.getType();
        this.name = service.getName();
    }
}

LiveLinearServiceView类 具体的ServiceView类,我需要映射。

public class LiveLinearServiceView extends ServiceView {

    public String logoURL;

    protected LiveLinearServiceView() {
        super();
    }

    @JsonCreator
    public LiveLinearServiceView(@JsonProperty("id")Long id, @JsonProperty("type")ServiceType type, @JsonProperty("name")String name, @JsonProperty("logoURL")String logoURL) {
        super(id, type, name);
        this.logoURL = logoURL;
    }

    public LiveLinearServiceView(LiveService service) {
        super(service);
        this.logoURL = service.getLogoURL().toString();
    }
}

我的主要问题是,为什么这种反序列化不起作用?还有一个额外的问题,为什么&#34;类型&#34;参数在输出中生成两次?

2 个答案:

答案 0 :(得分:1)

SubscriptionProductView.setServices引用List<Service>;杰克逊不知道如何实例化抽象类列表。有关解决方案,请参阅this

BTW,+1在问题和实际代码中有详细信息;这就是问题的问题。

答案 1 :(得分:1)

将以下方法添加到SubscriptionProductView可以解决问题

@JsonSetter 
public void setServices(List<ServiceView> services) { 
    this.services = services; 
} 

问题在于Abhijit说,杰克逊不知道如何使用

来映射服务
public void setServices(List<Service> services) {}