将多态对象列表反序列化到“对象”字段中

Deserialize List of polymorphic objects into Object field

提问人:Aleksandr Kravets 提问时间:8/10/2020 更新时间:8/17/2020 访问量:2256

问:

我的 REST 端点返回包含 Object 字段的 Response 对象。序列化一切正常,但是当我开始为此 API 编写客户端时,我遇到了反序列化问题。我根据一些关于 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());
        }
    }
}

我期望的(以及我所拥有的 Zoo#animals 类型)在输出中:List<Animals>

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

我现在对 Object 有什么:

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

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

java json jackson 多态反序列化

评论

0赞 Bernard 8/10/2020
当您将其反序列化为 ?List<? extends Animal>
0赞 Aleksandr Kravets 8/10/2020
@Bernard是的,它以这种方式工作。
0赞 Simon G. 8/12/2020
问题是杰克逊没有办法知道不能取值 .您已经定义了 a 来指示如何确定 an 的类型,但您根本没有限制。为什么您的端点需要返回一个?如果您可以指定您实际需要处理的对象类型,如果我知道的话,我愿意提供一个答案来说明如何完成。Zoo.animalsList<LinkedHashMap>MixInAnimalanimalsObject
0赞 Aleksandr Kravets 8/14/2020
@SimonG。我那里有对象,因为在实际应用程序中它是类似 rpc 协议的包装类。我怀疑我可以为 Object 类注册另一个 mixin 并在那里指定我需要的所有类,例如 Animal、ArrayList 等。不过还没有尝试过。

答:

0赞 jccampanero 8/14/2020 #1

如果您知道要转换为的类型,则可以使用 的方法。convertValueObjectMapper

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

请看以下示例:

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 的默认类型(并采取必要的预防措施)。

你可以这样做:

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

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

您可以做的另一种想法是为包含任意信息的字段定义自定义。Deserializer

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

当您定义为您的属性时,Jackson 会将类的实例指定为其。Objectcom.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializerDeserializer

在您的例子中,这个类将首先处理数组,因此我们获得一个作为主要的反序列化结果。List

这将由类的实例组成。为什么?因为在它们的实现中,对于数组中的每个 JSON 对象,它们都会生成一个 with 键、JSON 对象属性名称(按它们出现的顺序命名)和 values(一个带有 JSON 对象属性值的值)。ListLinkedHashMapLinkedHashMapString[]

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

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

评论

0赞 Aleksandr Kravets 8/14/2020
这是我在测试中用作解决方法的方法。但我绝对不希望在主代码中显式转换。
0赞 jccampanero 8/14/2020
@AleksandrKravets没关系。如果可以的话,您能否描述一下最终的用例,以尝试理解问题和可能的解决方案?
0赞 Aleksandr Kravets 8/14/2020
我有包装类,就像 Zoo 一样,我将其用作任意信息的容器,以及描述该信息操作的字段“方法”。我使用这个对象作为泽西资源的返回类型。它正在被杰克逊序列化为 json。没问题。在客户端,我必须反序列化这个json并获得完全相同的对象结构以供进一步使用。但是我得到了LinkedHashMaps:(。如果这还不够,请提出问题。
0赞 jccampanero 8/14/2020
@AleksandrKravets没关系。所以,任意信息就是你正在建模的,不是吗?您正在从客户端 Java 代码中使用这些信息,对吗?Object
0赞 Aleksandr Kravets 8/14/2020
正确。这就是我正在做的事情
0赞 Enerccio 8/15/2020 #2

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

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(这就是为什么有注释的原因),但应该可以与 jackson 一起使用。我没有包括类,但你肯定可以通过签名来制作自己的类或省略它(我需要序列化对)。@ExposePair

评论

0赞 Aleksandr Kravets 8/17/2020
您说“反序列化程序接受字段声明类型的类型”。但是反序列化可以完美地与 Animal 的单个对象一起使用。Jackson 知道正确类型的唯一方法是从 Mixin 中的类型映射中获取,对吧?如果它查看对象字段的类型映射,那么为什么它不对列表中的对象执行此操作?
0赞 Stephan Schlecht 8/16/2020 #3

要求是将 Object an 类型的属性反序列化为正确的类。

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

解决方案可能是向 JSON 数据添加“response_type”。

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

所以代替

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

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

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

作为输出。

0赞 Suman 8/16/2020 #4

如果不使 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;
    }

那么它肯定会起作用。

评论

0赞 Aleksandr Kravets 8/17/2020
那会有什么帮助?我不明白。
0赞 Suman 8/17/2020
animals 变量将使用 AnimalsDeserializer 类来类型转换。当它尝试时,它将首先键入 cast to List<Animal>将其传递给 convert 方法,然后它将返回与 Object 相同的结果。这将满足您的要求。我已经在本地尝试过了,它有效。
0赞 Aleksandr Kravets 8/17/2020 #5

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

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)是最具鼓舞人心的答案的作者。再次感谢!