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

时间:2020-08-10 11:25:17

标签: java json jackson polymorphic-deserialization

我的REST端点返回包含对象字段的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());
        }
    }
}

输出中的期望值(以及我将List<Animals>作为Zoo#animals类型得到的结果)

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

我现在拥有的对象:

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

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

5 个答案:

答案 0 :(得分:0)

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

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

请考虑以下示例:

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会将类com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer的实例分配为其Deserializer

在您的情况下,此类将首先处理数组,因此我们获得List作为主要反序列化结果。

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

此过程由mapObject方法处理。如果您有某种字段可以标识正在处理的Object的类型,则可以定义一个新的Deserializer来扩展UntypedObjectDeserializer并覆盖mapObject仅通过应用Java Bean内省和属性设置器,即可从其父级返回的Map创建实际的具体对象的方法。

这最后一种方法也可以用Converter来实现。实际上,它们非常适合于这两个阶段的过程。其背后的想法与上一段所述相同:您将收到ListMap中的ObjectMapper,如果您能够识别具体类型,则只需要从中构建相应的实例即可。收到的信息。您甚至可以在最后的转换步骤中使用<Helmet dangerouslySetInnerHTML={{ __html: yoastHead }} />

答案 1 :(得分:0)

不幸的是,反序列化器接受字段声明类型的类型。因此,如果字段为Object,则它不会反序列化为任何内容并将其保留为Map。对于这种情况,没有简单的解决方案,因为反序列化没有有关应为哪种类型的信息。一种解决方案是自定义映射器,就像答案之一一样,或者您可以使用自定义TaggedObject类而不是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类,但是您可以肯定地通过签名创建自己的类,也可以忽略它(我需要序列化对)。

答案 2 :(得分:0)

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

可以考虑对不同类型使用不同的路由。如果这不是一种选择,并且由于您可以控制序列化的数据并尽可能保持与现有代码的距离,则可以使用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

如果您将类似这样的内容用作输入(假设相应的类以类似于动物类的形式存在):

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

作为输出。

答案 3 :(得分:0)

没有将 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;
    }

那么它肯定会工作。

答案 4 :(得分:0)

我对所有参加活动的人表示真诚的感谢。我决定采用一些内省的方法,并编写了自定义反序列化程序,该程序在list的第一个对象内部查找,以确定是否需要将其反序列化为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)颁发,是最具启发性答案的作者。再次感谢!