当同时使用多态类型和构建器时,Jackson 反序列化不起作用

Jackson de-serialization doesn't work when both polymorphic types and builders are used

给定的测试失败,但我认为它不应该。

当数据对象转换为常规非生成器时 类,测试通过(来源:https://pastebin.com/pBkTb6HW)。

要使构建器对象通过测试,必须在 Animal 接口上添加 @JsonTypeInfo 注释。这意味着 Zoo 不能完全通用,但需要所有动物的通用超类型。

好像不应该存在这种差异?

杰克逊版本:2.10

错误:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `JacksonTest$Animal` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (String)"{"animals":[{"@type":"Dog","name":"doggo"},{"@type":"Cat","name":"cat"}]}"; line: 1, column: 13] (through reference chain: JacksonTest$Zoo$Builder["animals"]->java.util.ArrayList[0])

完整测试用例:

public class JacksonTest {

    @Test
    void test() throws JsonProcessingException {
        ObjectMapper m = new ObjectMapper();
        m.findAndRegisterModules();

        List<Animal> animals = List.of(
            Dog.builder().name("doggo").build(),
            Cat.builder().name("cat").build()
        );
        Zoo z = Zoo.builder().animals(animals).build();

        String json = m.writeValueAsString(z);
        Zoo deser = m.readValue(json, Zoo.class);
        assertThat(z).isEqualTo(deser);
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Zoo.Builder.class)
    static class Zoo {
        @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
        List<Animal> animals;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {}
    }

    interface Animal {
        String getName();
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Dog.Builder.class)
    static class Dog implements Animal {
        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {}
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Cat.Builder.class)
    static class Cat implements Animal {
        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {}
    }
}

由于我们想避免添加到Animals接口,我们可以通过添加接口ZooBuilder来解决这个问题,然后我们将@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)注释添加到animals 增变器方法并让 Zoo.Builder 实现 ZooBuilder

版本:

  • 采用OpenJDK 14
  • 日食:2020-03 (4.15.0)
  • junit: 5.6.2
  • log4j2: 2.13.3
  • 杰克逊:2.11.0
  • 龙目岛:1.18.12

package io.jeffmaxwell.Whosebug;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;

import lombok.Builder;
import lombok.Value;
import lombok.extern.log4j.Log4j2;

@Log4j2
public class Q62193465 {

    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @BeforeAll
    static void findAndRegistrerModules() {
        OBJECT_MAPPER.findAndRegisterModules();
    }

    @Test
    void test() throws JsonProcessingException {

        List<Animal> animals = List.of(Dog.builder()
                .name("doggo")
                .build(),
                Cat.builder()
                .name("cat")
                .build());

        var zoo = Zoo.builder()
                .animals(animals)
                .build();

        var zooAsJsonString = OBJECT_MAPPER.writeValueAsString(zoo);
        LOGGER.info("zooAsJsonString {}", zooAsJsonString);
        var zooFromJsonString = OBJECT_MAPPER.readValue(zooAsJsonString, Zoo.class);

        assertEquals(zoo, zooFromJsonString);
    }

    //Added
    interface ZooBuilder {

        @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
        Zoo.Builder animals(final List<Animal> animals);
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Zoo.Builder.class)
    public static class Zoo {
        @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
        List<Animal> animals;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder implements ZooBuilder {

        }
    }
    interface Animal {
        String getName();
    }

    @Value
    @Builder(builderClassName = "Builder")

    @JsonDeserialize(builder = Dog.Builder.class)
    static class Dog implements Animal {

        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {
        }
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Cat.Builder.class)
    static class Cat implements Animal {

        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {
        }
    }
}

备选方案

这避免了接口,但风险更大,因为它与更多 Lombok 生成的代码交互。


package io.jeffmaxwell.Whosebug;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;

import lombok.Builder;
import lombok.Value;
import lombok.extern.log4j.Log4j2;

@Log4j2
public class Q62193465 {

    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    @BeforeAll
    static void findAndRegistrerModules() {
        OBJECT_MAPPER.findAndRegisterModules();
    }

    @Test
    void test() throws JsonProcessingException {

        List<Animal> animals = List.of(Dog.builder()
                .name("doggo")
                .build(),
                Cat.builder()
                .name("cat")
                .build());

        var zoo = Zoo.builder()
                .animals(animals)
                .build();

        var zooAsJsonString = OBJECT_MAPPER.writeValueAsString(zoo);
        LOGGER.info("zooAsJsonString {}", zooAsJsonString);
        var zooFromJsonString = OBJECT_MAPPER.readValue(zooAsJsonString, Zoo.class);

        assertEquals(zoo, zooFromJsonString);
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Zoo.Builder.class)
    public static class Zoo {
        @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
        List<Animal> animals;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {

            @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
            public Builder animals(List<Animal> animals) {
                this.animals = animals;
                return this;
            }
        }
    }
    interface Animal {
        String getName();
    }

    @Value
    @Builder(builderClassName = "Builder")

    @JsonDeserialize(builder = Dog.Builder.class)
    static class Dog implements Animal {

        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {
        }
    }

    @Value
    @Builder(builderClassName = "Builder")
    @JsonDeserialize(builder = Cat.Builder.class)
    static class Cat implements Animal {

        String name;

        @JsonPOJOBuilder(withPrefix = "")
        public static class Builder {
        }
    }
}