Ohhnews

分类导航

$ cd ..
Baeldung原文

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

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

[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. 定义基于属性的创建器

对于不可变类,我们可以显式定义 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 属性

将构造函数配置为创建器后,我们就可以在保持类不可变的同时反序列化 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 包导入的创建器注解。

4.1. 将工厂方法作为创建器

工厂方法也可以作为创建器。如果 Jackson 没有将我们的工厂方法识别为创建器,它将抛出同样的 InvalidDefinitionException

要解决该问题,我们以与带参构造函数相同的方式注解该方法及其参数:

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

现在,这个方法也可以充当创建器。

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 反序列化异常,该异常发生在 Jackson 无法确定如何创建目标对象时。随后,我们探讨了几种解决它的方法。

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

文章 解决异常:无法从对象值反序列化(没有基于委托或基于属性的创建器) 首次出现在 Baeldung