JacksonでOptionalを使用する
1. 前書き
この記事では、Optionalクラスの概要を説明した後、Jacksonで使用するときに発生する可能性のあるいくつかの問題について説明します。
これに続いて、JacksonがOptionalsを通常のnull許容オブジェクトであるかのように処理するソリューションを紹介します。
2. 問題の概要
まず、JacksonでOptionalsをシリアル化および逆シリアル化しようとするとどうなるかを見てみましょう。
2.1. メーベン依存
ジャクソンを使用するには、its latest versionを使用していることを確認しましょう。
com.fasterxml.jackson.core
jackson-core
2.9.6
2.2. 私たちの本のオブジェクト
次に、1つの通常フィールドと1つのOptionalフィールドを含むクラスBook,を作成しましょう。
public class Book {
String title;
Optional 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"));
最後に、JacksonObjectMapperを使用してシリアル化してみましょう。
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. 溶液
ジャクソンが空のOptionalをnull,として扱い、現在のOptionalをその値を表すフィールドとして扱うことが必要です。
幸いなことに、この問題は解決されています。 Optionalを含むJackson has a set of modules that deal with JDK 8 datatypes。
3.1. Mavenの依存関係と登録
まず、最新バージョンをMaven依存関係として追加しましょう。
com.fasterxml.jackson.datatype
jackson-datatype-jdk8
2.9.6
これで、モジュールを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. 逆シリアル化
それでは、逆シリアル化のテストを繰り返しましょう。 ブックを読み直すと、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を通常のフィールドとして扱う方法を示しました。 。
これらの例の実装はover on GitHubにあります。これはMavenベースのプロジェクトであるため、そのまま実行するのは簡単です。