将多态对象列表反序列化为对象字段

Deserialize List of polymorphic objects into Object field

我的 REST 端点 returns 包含对象字段的响应对象。序列化一切都很好,但是当我开始为此 API 编写客户端时,我遇到了反序列化问题。我根据一些 questions/articles 关于 Jackson 的多态序列化制作了这个示例代码。它证明了这个问题。

@Data
abstract class Animal {

    String name;
}

@Data
class Dog extends Animal {

    boolean canBark;
}

@Data
class Cat extends Animal {

    boolean canMeow;
}

@Data
public class Zoo {

    private Object animals;
}


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "name", visible = true)
@JsonSubTypes({
        @JsonSubTypes.Type(name = "dog", value = Dog.class),
        @JsonSubTypes.Type(name = "cat", value = Cat.class)
})
public class Mixin {

}

public class Main {

    private static final String JSON_STRING = "{\n"
            + "  \"animals\": [\n"
            + "    {\"name\": \"dog\"},\n"
            + "    {\"name\": \"cat\"}\n"
            + "  ]\n"
            + "}";

    public static void main(String[] args) throws IOException {
        ObjectMapper objectMapper = Jackson.newObjectMapper()
                .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
                .setDefaultPropertyInclusion(Include.NON_NULL);
        objectMapper.addMixIn(Animal.class, Mixin.class);
        Zoo zoo = objectMapper.readValue(JSON_STRING, Zoo.class);
        for (Object animal : (Collection) zoo.getAnimals()) {
            System.out.println(animal.getClass());
        }
    }
}

我在输出中的期望(以及我对 List<Animals> 作为 Zoo#animals 类型的期望):

class jackson.poly.Dog
class jackson.poly.Cat

我现在拥有的对象:

class java.util.LinkedHashMap
class java.util.LinkedHashMap

但我需要反序列化除动物列表之外的其他类型的对象。请帮忙!

如果您知道要转换的类型,可以使用 ObjectMapper 的方法 convertValue

您可以转换单个值或整个列表。

考虑以下示例:

public static void main(String[] args) throws IOException {
  ObjectMapper objectMapper = Jackson.newObjectMapper()
    .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
    .setDefaultPropertyInclusion(Include.NON_NULL);
  objectMapper.addMixIn(Animal.class, Mixin.class);
  Zoo zoo = objectMapper.readValue(JSON_STRING, Zoo.class);
  List<Animal> animals = objectMapper.convertValue(zoo.getAnimals(), new TypeReference<List<Animal>>(){});
  for (Object animal : animals) {
    System.out.println(animal.getClass());
  }
}

对于您的评论,如果您不确定需要处理的实际类型,一个有用的选项是启用 Jackson 的默认类型(使用 required precautions)。

你可以这样做:

PolymorphicTypeValidator validator = BasicPolymorphicTypeValidator
  .builder()
  .allowIfBaseType(Animal.class)
  .build()
;

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(DefaultTyping.NON_FINAL, validator);

另一个认为您可以做的是为包含您的任意信息的字段定义一个自定义 Deserializer

您可以重用一些代码,而不是从头开始创建一个。

当您将 属性 定义为 Object 时,Jackson 将分配一个 class com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer 的实例作为它的Deserializer

在你的例子中,这个class将首先处理数组,所以我们得到一个List作为主要的反序列化结果。

List 将由 class LinkedHashMap 的实例组成。为什么?因为在他们的实现中,对于数组中的每个 JSON 对象,他们都会生成一个带有键的 LinkedHashMap,JSON 对象 属性 名称按它们出现的顺序,以及值,具有 JSON 对象 属性 值的 String[]

这个过程由mapObject方法处理。如果您有某种字段允许您识别正在处理的 Object 的类型,也许您可​​以定义一个新的 Deserializer 来扩展 UntypedObjectDeserializer 并覆盖 mapObject方法从其父级返回的 Map 创建实际的、具体的对象,只需应用 Java Bean 自省和 属性 设置器。

最后一种方法也可以用 Converters 来实现。它们实际上非常适合这个两阶段过程。这背后的想法与上一段描述的相同:你将收到一个 ListMap ,如果你能够识别具体类型,你只需要从中构建相应的实例收到的信息。您甚至可以在最后一个转换步骤中使用 ObjectMapper

不幸的是,反序列化器接受来自字段声明类型的类型。因此,如果字段为 Object,则它不会反序列化为任何内容并将其保留为 Map。对此没有简单的解决方案,因为反序列化没有关于它应该是什么类型的信息。一种解决方案是自定义映射器,就像其中一个答案一样,或者您可以使用自定义 TaggedObject class 而不是 Object,这是我在类似用例中使用的:

public class TaggedObject {

    @Expose
    private String type;
    @Expose
    private Object value;
    @Expose
    private Pair<TaggedObject, TaggedObject> pairValue;
    
    @SuppressWarnings("unchecked")
    public TaggedObject(Object v) {
        this.type = getTypeOf(v);
        if (v instanceof Pair) {
            this.pairValue = tagPair((Pair<Object, Object>) v);
            this.value = null;
        } else if ("long".equals(type)){
            this.value = "" + v;
            this.pairValue = null;
        } else {
            this.value = v;
            this.pairValue = null;
        }
    }

    private static Pair<TaggedObject, TaggedObject> tagPair(Pair<Object, Object> v) {
        return new Pair<TaggedObject, TaggedObject>(TaggedObject.tag(v.first), TaggedObject.tag(v.second));
    }

    private static String getTypeOf(Object v) {
        Class<?> cls = v.getClass();
        if (cls.equals(Double.class))
            return "double";
        if (cls.equals(Float.class))
            return "float";
        if (cls.equals(Integer.class))
            return "integer";
        if (cls.equals(Long.class))
            return "long";
        if (cls.equals(Byte.class))
            return "byte";
        if (cls.equals(Boolean.class))
            return "boolean";
        if (cls.equals(Short.class))
            return "short";
        if (cls.equals(String.class))
            return "string";
        if (cls.equals(Pair.class))
            return "pair";
        return "";
    }

    public static TaggedObject tag(Object v) {
        if (v == null)
            return null;
        return new TaggedObject(v);
    }

    public Object get() {
        if (type.equals("pair"))
            return new Pair<Object, Object>(
                    untag(pairValue.first),
                    untag(pairValue.second)
                    );
        return getAsType(value, type);
    }

    private static Object getAsType(Object value, String type) {
        switch (type) {
        case "string" : return value.toString();
        case "double" : return value;
        case "float"  : return ((Double)value).doubleValue();
        case "integer": return ((Double)value).intValue();
        case "long"   : {
            if (value instanceof Double)
                return ((Double)value).longValue();
            else
                return Long.parseLong((String) value);
        }
        case "byte"   : return ((Double)value).byteValue();
        case "short"  : return ((Double)value).shortValue();
        case "boolean": return value;
        }
        return null;
    }

    public static Object untag(TaggedObject object) {
        if (object != null)
            return object.get();
        return null;
    }
}

这是针对 google gson 的(这就是为什么有 @Expose 注释),但应该可以很好地与 jackson 一起使用。我没有包括 Pair class 但你当然可以根据签名制作你自己的签名或省略它(我需要序列化对)。

要求将 属性 类型的对象反序列化为正确的 class。

可以考虑针对不同的类型使用不同的路由。如果这不是一个选项,并且由于您可以控制序列化数据并尽可能接近现有代码,您可以使用 JsonDeserialize 注释。

解决方案可能是在 JSON 数据中添加 'response_type'。

由于您需要反序列化除动物列表之外的其他类型的对象,因此使用更通用的名称而不是动物名称可能更有意义。让我们将它从 animals 重命名为 response.

所以不用

private Object animals;

你将拥有:

private Object response;

然后你的代码稍微修改一下上面的几点可能看起来像这样:

package jackson.poly;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

public class Zoo {

    private String responseType;

    @JsonDeserialize(using = ResponseDeserializer.class)
    private Object response;

    public String getResponseType() { return responseType; }
    public Object getResponse() { return response; }
}

ResponseDeserializer 可能如下所示:

package jackson.poly;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;

import java.io.IOException;
import java.util.Arrays;

public class ResponseDeserializer extends JsonDeserializer<Object> {
    @Override
    public Object deserialize(JsonParser jsonParser,
                                  DeserializationContext deserializationContext) throws
            IOException, JsonProcessingException {
        Zoo parent = (Zoo)deserializationContext.getParser().getParsingContext().getParent().getCurrentValue();

        String responseType = parent.getResponseType();
        if(responseType.equals("Animals[]")) {
            Animal[] animals = jsonParser.readValueAs(Animal[].class);
            return Arrays.asList(animals);
        }
        else if(responseType.equals("Facilities[]")) {
            Facility[] facilities = jsonParser.readValueAs(Facility[].class);
            return Arrays.asList(facilities);
        }
        throw new RuntimeException("unknown subtype " + responseType);
    }

}

像这样使用输入数据(注意 response_type 属性):

private static final String JSON_STRING = "{\n"
        + "  \"response_type\": \"Animals[]\",\n"
        + "  \"response\": [\n"
        + "    {\"name\": \"dog\"},\n"
        + "    {\"name\": \"cat\"}\n"
        + "  ]\n"
        + "}";

对上述程序进行测试 运行 会在调试控制台上产生以下输出:

class jackson.poly.Dog
class jackson.poly.Cat

如果您使用类似这样的东西作为输入(假设相应的 classes 以类似于动物 classes 的形式存在):

private static final String JSON_STRING = "{\n"
        + "  \"response_type\": \"Facilities[]\",\n"
        + "  \"response\": [\n"
        + "    {\"name\": \"house\"},\n"
        + "    {\"name\": \"boat\"}\n"
        + "  ]\n"
        + "}";

你会得到

class jackson.poly.House
class jackson.poly.Boat

作为输出。

如果不将 animals 变量设为 List 类型,对象映射器很难理解为 animals 定义了子类型。

如果没有,请编写您自己的序列化程序,如下所示。

    class AnimalsDeserializer extends StdConverter<List<Animal>, Object> {

        @Override
        public Object convert(List<Animal> animals) {
            return animals;
        }
    }

用@JsonDeserialize 注释动物

    @Data
    class Zoo {

        @JsonDeserialize(converter = AnimalsDeserializer.class)
        private Object animals;
    }

那一定行得通

对所有参与的人表示衷心的感谢。我决定采用一些内省的方法,并编写了自定义反序列化程序,它查看列表中的第一个对象,以确定是否需要将其反序列化为列表。

public class AnimalsDeserializer extends JsonDeserializer<Object> {

    private Set<String> subtypes = Set.of("dog", "cat");

    @Override
    public Object deserialize(JsonParser parser, DeserializationContext ctx)
            throws IOException, JsonProcessingException {
        if (parser.getCurrentToken() == JsonToken.START_ARRAY) {
            TreeNode node = parser.getCodec().readTree(parser);
            TreeNode objectNode = node.get(0);
            if (objectNode == null) {
                return List.of();
            }
            TreeNode type = objectNode.get("name");
            if (type != null
                    && type.isValueNode()
                    && type instanceof TextNode
                    && subtypes.contains(((TextNode) type).asText())) {
                return parser.getCodec().treeAsTokens(node).readValueAs(new TypeReference<ArrayList<Animal>>() {
                });
            }
        }
        return parser.readValueAs(Object.class);
    }
}

也许有些检查是多余的。它有效,这才是最重要的:)。 奖项授予 StephanSchlecht 作为最鼓舞人心的答案的作者。再次感谢!