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ベースのプロジェクトなので、そのまま実行するのは簡単なはずです。