Ohhnews

分类导航

$ cd ..
Baeldung原文

解决Jackson反序列化异常:无法从对象值反序列化(缺少委托或属性创建器)

#jackson#java#json反序列化#异常处理#注解

[LOADING...]

1. 引言

Jackson 是一个 Java 库,常用于在 Java 对象与 JSON 之间进行转换。在 JSON 反序列化过程中,当 Jackson 无法确定如何创建目标类的实例时,可能会抛出 InvalidDefinitionException 即使 JSON 结构与类字段匹配,也可能发生这种情况。

在本教程中,我们将使用 Jackson 2.17.2 复现该异常,并探讨几种解决方法。

2. 复现异常

让我们从一个包含带参构造函数的不可变 Notification 类开始:

$ java
public class Notification {
    private final String message;
    private final int priority;
    public Notification(String message, int priority) {
        this.message = message;
        this.priority = priority;
    }
    // getters
}

接下来,尝试将 JSON 对象反序列化为 Notification 类:

$ java
String json = "{\"message\":\"Server maintenance\",\"priority\":2}";
ObjectMapper mapper = new ObjectMapper();
mapper.readValue(json, Notification.class);

尽管 JSON 属性与 Notification 中的字段匹配,Jackson 仍无法确定它们如何映射到构造函数参数。构造函数没有指定每个参数应使用哪个 JSON 属性。

因此,反序列化失败并抛出 InvalidDefinitionException

$ plaintext
Cannot construct instance of `com.baeldung.invaliddefinitionexception.Notification` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

该异常表明 Jackson 需要一种合适的机制来创建 Notification 实例。 我们可以通过多种方式解决。

3. 添加默认构造函数

解决该异常的一种方法是提供无参构造函数

$ java
public class NotificationWithDefaultConstructor {
    private String message;
    private int priority;
    public NotificationWithDefaultConstructor() {
    }
    // getters and setters
}

默认情况下,Jackson 依赖无参构造函数来实例化目标类。然后它可以在反序列化过程中填充对象属性

$ java
NotificationWithDefaultConstructor notification = objectMapper.readValue(json, NotificationWithDefaultConstructor.class);
assertEquals("Server maintenance", notification.getMessage());
assertEquals(2, notification.getPriority());

这种方法适用于可变类,Jackson 可以在创建实例后设置属性。

4. 定义基于属性的 Creator

对于不可变类,我们可以显式定义 Jackson 应如何使用带参构造函数

$ java
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class NotificationWithCreator {
    private final String message;
    private final int priority;
    @JsonCreator
    public NotificationWithCreator(
      @JsonProperty("message") String message, 
      @JsonProperty("priority") int priority) {
        this.message = message;
        this.priority = priority;
    }
    
    // getters
}

在这种情况下,@JsonCreator 注解告诉 Jackson 使用被注解的构造函数创建对象。** @JsonProperty 注解则指定每个构造函数参数对应哪个 JSON 属性**。

将构造函数配置为 Creator 后,我们可以在保持类不可变的同时反序列化 JSON:

$ java
NotificationWithCreator notification = objectMapper.readValue(json, NotificationWithCreator.class);
assertEquals("Server maintenance", notification.getMessage());
assertEquals(2, notification.getPriority());

此外,应确保从 com.fasterxml.jackson.annotation 包导入注解,Jackson 3.x 也使用该包。Jackson 不会识别从旧版 org.codehaus.jackson 包导入的 Creator 注解。

4.1. 将工厂方法作为 Creator

工厂方法也可以作为 Creator。如果 Jackson 不将我们的工厂方法识别为 Creator,它将抛出相同的 InvalidDefinitionException

要解决该问题,我们像处理带参构造函数一样,为该注解标注方法及其参数:

$ java
@JsonCreator
public static NotificationWithFactory create(
  @JsonProperty("message") String message,
  @JsonProperty("priority") int priority) {
    return new NotificationWithFactory(message, priority);
}

现在,这个方法也可以充当 Creator。

5. 发现构造函数参数名

最后,除了向目标类添加 Jackson 注解外,我们还可以配置 Jackson 来发现构造函数参数名。

5.1. 使用 ParameterNamesModule

在 Jackson 2.x 中,我们可以使用 ParameterNamesModule 来发现构造函数参数名,而无需添加 @JsonProperty 注解:

$ java
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParameterNamesModule());
Notification notification = mapper.readValue(json, Notification.class);
assertEquals("Server maintenance", notification.getMessage());
assertEquals(2, notification.getPriority());

我们需要使用 Java -parameters 选项编译该类,以便参数名在运行时可用。 **在 Jackson 3.x 中,该功能内置于 jackson-databind 并默认启用,**因此无需注册单独的模块。

5.2. 使用 ParanamerModule

Jackson 2.x 中的另一个选择是使用 ParanamerModule。首先,需要将 jackson-module-paranamer 依赖添加到项目中:

$ xml
<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-paranamer</artifactId>
    <version>${jackson.version}</version>
</dependency>

接下来,将 ParanamerModule 注册到 ObjectMapper

$ java
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ParanamerModule());
Notification notification =
    mapper.readValue(json, Notification.class);
assertEquals("Server maintenance", notification.getMessage());
assertEquals(2, notification.getPriority());

注册后,ParanamerModule 会为 Jackson 提供构造函数参数名,因此它可以将 message 和 priority 匹配到相应的 JSON 属性。

然而,Paranamer 已停止维护,且仅支持 Jackson 2.x,这限制了该方法在新项目中的使用。

6. 结论

在本文中,我们复现了 Jackson 在无法确定如何创建目标对象时发生的反序列化异常。随后,我们探讨了几种解决方法。

这些方法包括添加默认构造函数、使用 @JsonCreator@JsonProperty 定义基于属性的 Creator,以及让 Jackson 发现构造函数参数名。静态工厂方法也可以作为 Creator。

一如既往,完整源代码可在 GitHub 上获取