使用Jackson将JSON反序列化为多态类型 - 完整示例给我编译错误

129

我正在尝试完成Programmer Bruce的教程,目的是允许多态JSON的反序列化。

完整列表可以在这里找到:Programmer Bruce教程(非常棒的资料)

我已经顺利地完成了前五个,但在最后一个(示例6)遇到了问题,而这显然是我需要确保其正常运行的一部分。

我在编译时遇到了以下错误:

ObjectMapper类中的readValue(JsonParser, Class)方法对于参数 (ObjectNode, Class<capture#6-of ? extends Animal>) 不适用

这个错误是由下面这段代码引起的

  public Animal deserialize(  
      JsonParser jp, DeserializationContext ctxt)   
      throws IOException, JsonProcessingException  
  {  
    ObjectMapper mapper = (ObjectMapper) jp.getCodec();  
    ObjectNode root = (ObjectNode) mapper.readTree(jp);  
    Class<? extends Animal> animalClass = null;  
    Iterator<Entry<String, JsonNode>> elementsIterator =   
        root.getFields();  
    while (elementsIterator.hasNext())  
    {  
      Entry<String, JsonNode> element=elementsIterator.next();  
      String name = element.getKey();  
      if (registry.containsKey(name))  
      {  
        animalClass = registry.get(name);  
        break;  
      }  
    }  
    if (animalClass == null) return null;  
    return mapper.readValue(root, animalClass);
  }  
} 

具体来说,这行代码是

return mapper.readValue(root, animalClass);

有人遇到过这个问题吗?如果有,是否有解决方法?


1
你使用的是哪个Jackson版本?本教程假定使用的是Jackson 1.x,同时,您有没有选择基于注释的多态实例反序列化的原因? - jbarrueta
我正在使用2.5版本。我可以尝试降级到1.X版本来解决这个问题。此外,您能否推荐一个教程或示例,以展示如何使用注释来处理此问题? - Jon Driscoll
是的,我不建议您降级,我很乐意给出一个工作示例。 - jbarrueta
2
这里有另一篇文章,很好地解释了执行多态序列化/反序列化的不同方法:https://octoperf.com/blog/2018/02/01/polymorphism-with-jackson/ - Jerome L
我刚刚添加了一个(可以说是)更简单的解决方案,它根据属性的存在来处理反序列化为不同类型的问题:https://dev59.com/I2Af5IYBdhLWcg3wylQd#50013090 - bernie
7个回答

205

如承诺的那样,我将提供一个示例,展示如何使用注解对多态对象进行序列化/反序列化。这个示例是基于你正在阅读的教程中的Animal类。

首先需要在你的Animal类和其子类上加上Json注解。

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),

    @JsonSubTypes.Type(value = Cat.class, name = "Cat") }
)
public abstract class Animal {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

然后是你的子类,DogCat

public class Dog extends Animal {

    private String breed;

    public Dog() {

    }

    public Dog(String name, String breed) {
        setName(name);
        setBreed(breed);
    }

    public String getBreed() {
        return breed;
    }

    public void setBreed(String breed) {
        this.breed = breed;
    }
}

public class Cat extends Animal {

    public String getFavoriteToy() {
        return favoriteToy;
    }

    public Cat() {}

    public Cat(String name, String favoriteToy) {
        setName(name);
        setFavoriteToy(favoriteToy);
    }

    public void setFavoriteToy(String favoriteToy) {
        this.favoriteToy = favoriteToy;
    }

    private String favoriteToy;

}

如您所见,对于CatDog没有什么特殊之处,唯一了解它们的是Animal抽象类,因此在反序列化时,您将针对Animal并且ObjectMapper将返回实际的实例,如下面的测试所示:

public class Test {

    public static void main(String[] args) {

        ObjectMapper objectMapper = new ObjectMapper();

        Animal myDog = new Dog("ruffus","english shepherd");

        Animal myCat = new Cat("goya", "mice");

        try {
            String dogJson = objectMapper.writeValueAsString(myDog);

            System.out.println(dogJson);

            Animal deserializedDog = objectMapper.readValue(dogJson, Animal.class);

            System.out.println("Deserialized dogJson Class: " + deserializedDog.getClass().getSimpleName());

            String catJson = objectMapper.writeValueAsString(myCat);

            Animal deseriliazedCat = objectMapper.readValue(catJson, Animal.class);

            System.out.println("Deserialized catJson Class: " + deseriliazedCat.getClass().getSimpleName());



        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

运行 Test 类后的输出:

{"@type":"Dog","name":"ruffus","breed":"english shepherd"}

反序列化后的 dogJson 类:Dog

{"@type":"Cat","name":"goya","favoriteToy":"mice"}

反序列化后的 catJson 类:Cat


12
如果我从Retrofit获取对象时没有@type信息,该怎么办? - Renan Bandeira
18
没有使用@JsonSubTypes注解,是否可以实现这个功能?这样在编写原始类之后,就可以在其他包中添加子类了。 - Dave
3
@KamalJoshi 是的,如果Animal是一个接口,你也可以这样做。我已经通过将Animal更改为接口进行了测试,并且相同的主方法可以工作。关于你的第二个问题,注解中的“name”与“Dog”没有关系是正确的,“name”只是被序列化类的一个字段,可能为了避免混淆,我应该在Animal类中使用petName而不是name。Jackson注解中的“name”字段是要设置为序列化的JSON标识符的值。希望对你有所帮助。 - jbarrueta
3
我相信你可以实现自己的@JsonTypeIdResolver,它将使用反射来确定可能的子类列表并将你的@type绑定到它们的名称上。这里有一个示例实现:https://www.thomaskeller.biz/blog/2013/09/10/custom-polymorphic-type-handling-with-jackson/ - G. Kashtanov
7
我已经准备好了@JsonTypeIdResolver的代码片段,你可以在这个链接中找到(可能)符合你需求的代码: https://gist.github.com/root-talis/36355f227ff5bb7a057ff7ad842d37a3 - G. Kashtanov
显示剩余16条评论

35

虽然@jbarrueta答案完美无缺,但在Jackson的2.12版本中引入了一个备受期待的新类型DEDUCTION,用于@JsonTypeInfo注释。

当您无法更改传入的JSON或者不能更改时,它非常有用。我仍然建议使用use = JsonTypeInfo.Id.NAME,因为新方式在复杂情况下可能会抛出异常,因为它无法确定使用哪个子类型。

现在你可以简单地写

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
    @JsonSubTypes.Type(Dog.class),
    @JsonSubTypes.Type(Cat.class) }
)
public abstract class Animal {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

它将产生{"name":"ruffus", "breed":"english shepherd"}{"name":"goya", "favoriteToy":"mice"}

再次强调,如果某些字段可能不存在,比如breedfavoriteToy,使用NAME会更安全。


3
如果使用Jackson 2.12+,这是最合适的答案。 - Younes El Ouarti
1
不知道这个。我猜它可能会慢一些,因为需要一些分析,但我喜欢它的简单性。不过,为了安全起见,我仍然会使用JsonSubTypes({ Type(value = SomeType.class, name = "SOMENAME")。 - user2509192
1
谢谢您提供的内容。这是一个完美的例子,几乎正是我所需要的。有没有办法将对象本身作为包装对象来使用?在您的示例中,我希望看到每个创建的动物周围都有一个JSON包装对象,以便一个被包装为“Dog:{}”,另一个被包装为“Cat:{}”...这可能吗?我尝试在抽象类和子类上都使用include = JsonTypeInfo.As.WRAPPER_OBJECT,但无济于事。 - Jack F.
1
@JackF。很遗憾,我不知道如何解决你的问题,除非编写一个自定义反序列化程序,这是很痛苦的。第一种方法是创建新的DTO包装器并对其进行反序列化。第二种方法更加hacky,但是您可以创建一个通用的DTO包装器,在包装字段上放置@JsonAlias,以便它同时匹配“Cat”和“Dog”,然后像邪恶的疯子一样大笑。我会选择第一种方式,虽然支持起来有点繁琐,但它明确声明了其字段。此外,当有人决定向包装器DTO添加额外的字段时,这将更容易实现。 - Xobotun
1
此外,stackoverflow 上多次提到评论不适合用于讨论,而您的问题更像是一个独立的问题,因此我建议您将其编写为一个独立的帖子。这样您也会得到比我更好的答案。 :D - Xobotun

31

在类 Animal 的声明之前,只需要一行代码即可实现正确的多态序列化/反序列化:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public abstract class Animal {
   ...
}

这行代码的意思是: 在序列化时添加一个元属性或在反序列化时读取一个元属性 (include = JsonTypeInfo.As.PROPERTY),该属性名为"@class" (property = "@class"),它包含了Java类的完全限定名称 (use = JsonTypeInfo.Id.CLASS)。

因此,如果您直接创建JSON(而不是进行序列化),请记得添加元属性"@class"并填写所需的类名以进行正确的反序列化。

更多信息请参见此处


1
我知道我来晚了,这只有在序列化JSON的应用程序也是反序列化它的同一个应用程序时才能工作吗?如果您需要从一个应用程序序列化对象并在其他地方进行反序列化,那么使用JsonSubTypes的自定义名称->类映射是否更好?这样,无论哪个应用程序正在反序列化JSON,都可以使用其自定义映射与JsonSubTypes中给定的名称。换句话说,其他应用程序可以使用自己的类进行反序列化。(除非当然,在JSON中包含的类是简单名称,但我不知道它如何工作。) - user2509192
1
嗨@user2509192,你所说的肯定是在处理不同应用程序时需要考虑的事情。我试图回答最常见的用例。 - Marco
1
确实,这个解决方案更短,但不幸的是 Sonar 报告了“java:S4544”漏洞,并且存在远程代码执行的风险。我会采取被接受的答案。 - makozaki

5

通过Jackson库全局配置Jackson对象映射器(jackson.databind.ObjectMapper)添加信息,例如具体类类型,用于某些类(例如抽象类)的多态序列化/反序列化是一种简单的方法。

为此,请确保您的映射器已正确配置。例如:

选项1:支持抽象类(和Object类型类)的多态序列化/反序列化

jacksonObjectMapper.enableDefaultTyping(
    ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE); 

选项2:支持抽象类(和对象类型的类)以及这些类型的数组的多态序列化/反序列化。
jacksonObjectMapper.enableDefaultTyping(
    ObjectMapper.DefaultTyping.NON_CONCRETE_AND_ARRAYS); 

参考: https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization

这是关于IT技术方面的内容,主要是介绍如何使用Jackson来实现多态反序列化功能。如果您需要了解更多信息,请点击上面的链接。

1
我应该把这个放在哪里(我正在使用Hibernate)? - AndroidTank
在定义ObjectMapper的上下文中,在使用它之前。例如,如果您正在使用静态ObjectMapper,则可以添加一个静态段落并在其中设置这些值。 - AmitW
1
我没有使用ObjectMapper。它可以在没有任何定义的情况下正常工作。 - AndroidTank
您提供的链接似乎已经失效了... 是否可能现在的URL是https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization? - SJuan76
看起来SJuan76提供的链接现在可用了,但是你提供的链接包含相同的信息。 - AmitW

2
处理多态性要么是模型绑定,要么需要大量代码和各种自定义反序列化器。我是JSON动态反序列化库的共同作者,该库允许进行独立于模型的json反序列化。下面可以找到解决OP问题的方法。请注意,规则以非常简洁的方式声明。
public class SOAnswer {
    @ToString @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static abstract class Animal {
        private String name;    
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Dog extends Animal {
        private String breed;
    }

    @ToString(callSuper = true) @Getter @Setter
    @AllArgsConstructor @NoArgsConstructor
    public static class Cat extends Animal {
        private String favoriteToy;
    }
    
    
    public static void main(String[] args) {
        String json = "[{"
                + "    \"name\": \"pluto\","
                + "    \"breed\": \"dalmatian\""
                + "},{"
                + "    \"name\": \"whiskers\","
                + "    \"favoriteToy\": \"mouse\""
                + "}]";
        
        // create a deserializer instance
        DynamicObjectDeserializer deserializer = new DynamicObjectDeserializer();
        
        // runtime-configure deserialization rules; 
        // condition is bound to the existence of a field, but it could be any Predicate
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("breed"),
                DeserializationActionFactory.objectToType(Dog.class)));
        
        deserializer.addRule(DeserializationRuleFactory.newRule(1, 
                (e) -> e.getJsonNode().has("favoriteToy"),
                DeserializationActionFactory.objectToType(Cat.class)));
        
        List<Animal> deserializedAnimals = deserializer.deserializeArray(json, Animal.class);
        
        for (Animal animal : deserializedAnimals) {
            System.out.println("Deserialized Animal Class: " + animal.getClass().getSimpleName()+";\t value: "+animal.toString());
        }
    }
}

为pretius-jddl添加Maven依赖(请在maven.org/jddl检查最新版本):

<dependency>
  <groupId>com.pretius</groupId>
  <artifactId>jddl</artifactId>
  <version>1.0.0</version>
</dependency>

0

如果现有属性的名称不等于name,则可以使用注释值EXISTING_PROPERTY

例如,如果属性名称为type而不是name,则可以使用此注释:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
              include = JsonTypeInfo.As.EXISTING_PROPERTY,
              property = "type")

另请参阅 https://dev59.com/kmUo5IYBdhLWcg3wkAHB#62278471


-2
如果使用fasterxml库的话, 可能需要进行以下更改。
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;

在主方法中--

使用

SimpleModule module =
  new SimpleModule("PolymorphicAnimalDeserializerModule");

而不是

new SimpleModule("PolymorphicAnimalDeserializerModule",
      new Version(1, 0, 0, null));

在Animal的deserialize()函数中,进行以下更改。
//Iterator<Entry<String, JsonNode>> elementsIterator =  root.getFields();
Iterator<Entry<String, JsonNode>> elementsIterator = root.fields();

//return mapper.readValue(root, animalClass);
return  mapper.convertValue(root, animalClass); 

这适用于 fasterxml.jackson。如果它仍然抱怨类字段,请使用与 json 中字段名称相同的格式(带有"_"下划线)。因为这可能不受支持。
//mapper.setPropertyNamingStrategy(new CamelCaseNamingStrategy());
abstract class Animal
{
  public String name;
}

class Dog extends Animal
{
  public String breed;
  public String leash_color;
}

class Cat extends Animal
{
  public String favorite_toy;
}

class Bird extends Animal
{
  public String wing_span;
  public String preferred_food;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接