Jackson 使用 Optional
1. 简介
在本文中,我们将概述Optional 类,然后解释一些在与 Jackson 一起使用时可能遇到的问题。 在此之后,我们将介绍一个解决方案,让 Jackson 将Optionals视为普通的可空对象。
2. 问题概述
首先,让我们看看当我们尝试使用 Jackson序列化和反序列化Optionals时会发生什么。
2.1. Maven 依赖
要使用 Jackson,请确保我们使用的是最新版本 :
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.13.0</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.9.6</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");
如果我们尝试序列化一本空书,它将被存储为null:
book.setSubTitle(Optional.empty());
String serializedBook = mapper.writeValueAsString(book);
assertThat(from(serializedBook).getString("subTitle")).isNull();
3.3. 反序列化
现在,让我们重复我们的反序列化测试。如果我们重新阅读我们的书,我们将看到我们不再收到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());