JacksonでOptionalを使う
1前書き
この記事では、https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html[
Optional
]クラスの概要を説明した後、問題となる可能性のある問題について説明します。ジャクソンでそれを使用するときに遭遇する。
これに続いて、Jacksonに
Optionals
をあたかも通常のnull許容オブジェクトであるかのように扱わせるソリューションを紹介します。
2問題の概要
最初に、Jacksonで
Optionals
をシリアライズおよびデシリアライズしようとしたときに何が起こるかを見てみましょう。
2.1. Mavenの依存関係
Jacksonを使用するには、https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22com.fasterxml.jackson.core%22%20AND%20a%3A%を使用していることを確認してください。 22jackson-core%22[最新バージョン]:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.6</version>
</dependency>
2.2. 私たちの本のオブジェクト
それから、通常のフィールドと
Optional
フィールドを1つずつ含む__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);
Optional
フィールドの出力にはその値が含まれていませんが、代わりに
present
という名前のフィールドを持つネストされたJSONオブジェクトが含まれています。
{"title":"Oliver Twist","subTitle":{"present":true}}
これは奇妙に見えるかもしれませんが、それは実際に私たちが期待すべきことです。
この場合、
isPresent()
は
Optional
クラスのパブリックゲッターです。
つまり、空かどうかに応じて、
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
をその値を表すフィールドとして扱うことです。
幸いなことに、この問題は解決しました。
https://github.com/FasterXML/jackson-modules-java8
[Jacksonには、
Optional
を含む、JDK 8データ型を扱う一連のモジュールがあります。
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
オブジェクトをもう一度シリアル化しようとすると、入れ子になったJSONとは対照的に、__subtitleがあることがわかります。
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
を通常のフィールドとして扱う方法を示します。
これらの例の実装はhttps://github.com/eugenp/tutorials/blob/master/jackson/src/test/java/com/baeldung/jackson/miscellaneous/mixin/[GitHub上で動く]にあります。これはMavenベースのプロジェクトなので、そのまま実行するのは簡単なはずです。