1. 引言
本文將對 Optional 類進行概述,然後解釋我們在使用它與 Jackson 時可能遇到的問題。
隨後,我們將介紹一種解決方案,該解決方案可以讓 Jackson 將 Optionals 視為普通的空對象。
2. 問題概述
首先,讓我們看看嘗試使用 Jackson 序列化和反序列化 Optionals 時會發生什麼。
2.1. Maven 依賴
為了使用 Jackson,請確保我們使用的是其最新版本:https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.2</version>
</dependency>2.2. 我們的 Book 對象
然後,讓我們創建一個名為 Book 的類,該類包含一個普通字段和一個 可選 字段:
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);我們將會看到,可選 字段的輸出不包含其值,而是包含一個嵌套的 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 對象,我們會發現現在不再是嵌套的 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. 反序列化
現在,我們重新運行反序列化測試。如果重新閲讀我們的 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 作為普通字段處理。