使用 Optional 与 Jackson
最后更新:2023年8月18日
1. 简介
在本文中,我们将概述 Optional 类,然后解释在使用它与 Jackson 交互时可能遇到的一些问题。
接下来,我们将介绍一个解决方案,它将使 Jackson 将 Optionals 视为普通的nullable对象。
2. 问题概述
首先,让我们看看当我们尝试使用 Jackson 序列化和反序列化 Optionals 时会发生什么。
2.1. Maven 依赖
要使用 Jackson,让我们确保我们使用的是 它的最新版本
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.2</version>
</dependency>
2.2. 我们的 Book 对象
然后,让我们创建一个类 Book, 包含一个普通字段和一个 Optional 字段
public class Book {
String title;
Optional<String> subTitle;
// getters and setters omitted
}
请记住,Optionals 不应该用作字段,我们这样做是为了说明问题。
2.3. 序列化
现在,让我们实例化一个 Book
Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
最后,让我们尝试使用 Jackson ObjectMapper 序列化它
String result = mapper.writeValueAsString(book);
我们将看到 Optional 字段的输出不包含其值,而是一个嵌套的 JSON 对象,其中包含一个名为 present 的字段
{"title":"Oliver Twist","subTitle":{"present":true}}
虽然这可能看起来很奇怪,但实际上这是我们应该期望的。
在这种情况下,isPresent() 是 Optional 类上的一个公共 getter。这意味着它将以 true 或 false 的值序列化,具体取决于它是否为空。这是 Jackson 的默认序列化行为。
如果我们仔细思考,我们想要的是序列化 subtitle 字段的实际值。
2.4. 反序列化
现在,让我们反转之前的示例,这次尝试将对象反序列化到 Optional 中。我们将看到现在我们得到一个 JsonMappingException:
@Test(expected = JsonMappingException.class)
public void givenFieldWithValue_whenDeserializing_thenThrowException
String bookJson = "{ \"title\": \"Oliver Twist\", \"subTitle\": \"foo\" }";
Book result = mapper.readValue(bookJson, Book.class);
}
让我们查看堆栈跟踪
com.fasterxml.jackson.databind.JsonMappingException:
Can not construct instance of java.util.Optional:
no String-argument constructor/factory method to deserialize from String value ('The Parish Boy's Progress')
这种行为再次是有道理的。本质上,Jackson 需要一个构造函数,它可以将 subtitle 的值作为参数。这对于我们的 Optional 字段来说不是这种情况。
3. 解决方案
我们想要的是 Jackson 将空的 Optional 视为 null, 并将存在的 Optional 视为表示其值的字段。
幸运的是,这个问题已经为我们解决了。Jackson 有一组处理 JDK 8 数据类型的模块,包括 Optional。
3.1. Maven 依赖和注册
首先,让我们添加最新版本作为 Maven 依赖
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.13.3</version>
</dependency>
现在,我们只需要将模块注册到我们的 ObjectMapper 中
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());
3.2. 序列化
现在,让我们测试它。如果再次尝试序列化我们的 Book 对象,我们将看到现在有一个 subtitle, 而不是嵌套的 JSON
Book book = new Book();
book.setTitle("Oliver Twist");
book.setSubTitle(Optional.of("The Parish Boy's Progress"));
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle"))
.isEqualTo("The Parish Boy's Progress");
如果尝试序列化一个空的 book,它将被存储为 null
book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle")).isNull();
3.3. 反序列化
现在,让我们重复我们的反序列化测试。如果我们重新读取我们的 Book,我们将看到我们不再获得 JsonMappingException:
Book newBook = mapper.readValue(result, Book.class);
assertThat(newBook.getSubTitle()).isEqualTo(Optional.of("The Parish Boy's Progress"));
最后,让我们再次重复测试,这次使用 null. 我们将看到我们再次没有得到 JsonMappingException, 事实上,有一个空的 Optional:
assertThat(newBook.getSubTitle()).isEqualTo(Optional.empty());
4. 结论
我们已经展示了如何通过利用 JDK 8 DataTypes 模块来解决这个问题,演示了它如何使 Jackson 将空的 Optional 视为 null, 并将存在的 Optional 视为普通的字段。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















