Contents

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。这意味着它将使用值truefalse进行序列化,具体取决于它是否为空。这是 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());