これは、なにをしたくて書いたもの?
Jacksonを使ってオブジェクトをJSONにシリアライズする際に、null
のプロパティの出力は抑制したくなることがあります。
JSONにした時に、以下のような状態を
{ "property1": "value1", "property2": null }
こうする、という話ですね。
{ "property1": "value1" }
このやり方を忘れて毎回調べている気がするので、メモしておこうかなと。
ふつうにJacksonを使った場合、Spring Bootを使った場合、Quarkusを使った場合でそれぞれメモしていきます。
方法
大きく、2つの方法があります。
- @JsonIncludeアノテーションでInclude.NON_NULLを指定する
- ObjectMapper#setSerializationInclusionでInclude.NON_NULLを指定する
前者は対象となるプロパティを持つクラス、もしくはプロパティ自体に付与し、後者はデフォルトの設定として振る舞います。
Spring BootやQuarkusは、ObjectMapper#setSerializationInclusion
を設定ファイルで行えるようになっています。
- Spring Boot
- Quarkus
今回は、それぞれをJackson 2.14.1で試していこうかなと思います。Spring Boot 3.0.1、Quarkus 2.15.3.Finalでも含まれているJacksonの
バージョンは2.14.1になっています。
環境
今回の環境は、こちら。
$ java --version openjdk 17.0.5 2022-10-18 OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu122.04) OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu122.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-58-generic", arch: "amd64", family: "unix"
お題
書籍をお題にして、以下のようなクラスを対象にしたいと思います。
src/main/java/org/littlewings/jackson/nonnull/Book.java
package org.littlewings.jackson.nonnull; import java.util.Arrays; import java.util.List; public class Book { String isbn; String title; Integer price; Category category; List<Category> categories; public static Book create(String isbn, String title, Integer price, Category... categories) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); book.setCategory(categories.length > 0 ? categories[0] : null); book.setCategories(categories != null ? Arrays.asList(categories) : null); return book; } // getter/setterは省略 }
ネストしたプロパティおよびコレクションについても確認したかったので、カテゴリーを持つことにして、ちょっとわかりにくいですが
カテゴリーの最初のひとつを単独のプロパティで持つことにしました。
Category category; List<Category> categories;
カテゴリー側はこんな感じにします。
src/main/java/org/littlewings/jackson/nonnull/Category.java
package org.littlewings.jackson.nonnull; public class Category { Integer id; String name; public static Category create(Integer id, String name) { Category category = new Category(); category.setId(id); category.setName(name); return category; } // getter/setterは省略 }
Jackson単体で試してみる
まずは、Jackson単体で試します。
確認はテストコードで行いましょう。
準備
Maven依存関係など。
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.14.1</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.24.1</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build>
テストコードの雛形をこんな感じで作成。
src/test/java/org/littlewings/jackson/nonnull/JacksonIncludeNonNullTest.java
package org.littlewings.jackson.nonnull; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import static org.assertj.core.api.Assertions.assertThat; public class JacksonIncludeNonNullTest { // ここにテストを書く!! }
まずはふつうに
とりあえず、すべてのプロパティが非null
になるようにしてみます。
@Test void simply(TestInfo testInfo) throws JsonProcessingException { Book book = Book.create( " 978-4621303252", "Effective Java 第3版", 4400, Category.create(1, "java"), Category.create(2, "programming") ); ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(book); System.out.printf("%s: json = %s%n", testInfo.getDisplayName(), json); assertThat(json) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"title\":\"Effective Java 第3版\",\"price\":4400,\"category\":{\"id\":1,\"name\":\"java\"},\"categories\":[{\"id\":1,\"name\":\"java\"},{\"id\":2,\"name\":\"programming\"}]}"); }
System.out.printf
している部分の結果。
simply(TestInfo): json = {"isbn":" 978-4621303252","title":"Effective Java 第3版","price":4400,"category":{"id":1,"name":"java"},"categories":[{"id":1,"name":"java"},{"id":2,"name":"programming"}]}
整形後。
{ "isbn": " 978-4621303252", "title": "Effective Java 第3版", "price": 4400, "category": { "id": 1, "name": "java" }, "categories": [ { "id": 1, "name": "java" }, { "id": 2, "name": "programming" } ] }
nullを含めてみる
次に、null
を含めてみます。
@Test void withNull(TestInfo testInfo) throws JsonProcessingException { Book book1 = Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); Book book2 = Book.create( " 978-4621303252", null, null ); ObjectMapper mapper = new ObjectMapper(); String json1 = mapper.writeValueAsString(book1); String json2 = mapper.writeValueAsString(book2); System.out.printf("%s: json1 = %s%n", testInfo.getDisplayName(), json1); assertThat(json1) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"title\":null,\"price\":null,\"category\":{\"id\":null,\"name\":\"java\"},\"categories\":[{\"id\":null,\"name\":\"java\"},{\"id\":2,\"name\":null}]}"); System.out.printf("%s: json2 = %s%n", testInfo.getDisplayName(), json2); assertThat(json2) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"title\":null,\"price\":null,\"category\":null,\"categories\":[]}"); }
適当にいくつかプロパティをnull
にしておきます。
Book book1 = Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); Book book2 = Book.create( " 978-4621303252", null, null );
標準出力の書き出し結果。
withNull(TestInfo): json1 = {"isbn":" 978-4621303252","title":null,"price":null,"category":{"id":null,"name":"java"},"categories":[{"id":null,"name":"java"},{"id":2,"name":null}]} withNull(TestInfo): json2 = {"isbn":" 978-4621303252","title":null,"price":null,"category":null,"categories":[]}
整形後。
## json1 { "isbn": " 978-4621303252", "title": null, "price": null, "category": { "id": null, "name": "java" }, "categories": [ { "id": null, "name": "java" }, { "id": 2, "name": null } ] } ## json2 { "isbn": " 978-4621303252", "title": null, "price": null, "category": null, "categories": [] }
null
がそのまま出力されますね。なお、コレクションにnull
を指定すると空のコレクションになりましたね。
@JsonInclude(JsonInclude.Include.NON_NULL)を使う
では、@JsonInclude(JsonInclude.Include.NON_NULL)
を使ってみましょう。
src/main/java/org/littlewings/jackson/nonnull/BookNonNull.java
package org.littlewings.jackson.nonnull; import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class BookNonNull { String isbn; String title; Integer price; Category category; List<Category> categories; public static BookNonNull create(String isbn, String title, Integer price, Category... categories) { BookNonNull book = new BookNonNull(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); book.setCategory(categories.length > 0 ? categories[0] : null); book.setCategories(categories != null ? Arrays.asList(categories) : null); return book; } // getter/setterは省略 }
クラスに@JsonInclude(JsonInclude.Include.NON_NULL)
を付与。
@JsonInclude(JsonInclude.Include.NON_NULL) public class BookNonNull {
テストコード。
@Test void withNullExclude(TestInfo testInfo) throws JsonProcessingException { BookNonNull book1 = BookNonNull.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); BookNonNull book2 = BookNonNull.create( " 978-4621303252", null, null ); ObjectMapper mapper = new ObjectMapper(); String json1 = mapper.writeValueAsString(book1); String json2 = mapper.writeValueAsString(book2); System.out.printf("%s: json1 = %s%n", testInfo.getDisplayName(), json1); assertThat(json1) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"category\":{\"id\":null,\"name\":\"java\"},\"categories\":[{\"id\":null,\"name\":\"java\"},{\"id\":2,\"name\":null}]}"); System.out.printf("%s: json2 = %s%n", testInfo.getDisplayName(), json2); assertThat(json2) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"categories\":[]}"); }
標準出力の書き出し結果。
withNullExclude(TestInfo): json1 = {"isbn":" 978-4621303252","category":{"id":null,"name":"java"},"categories":[{"id":null,"name":"java"},{"id":2,"name":null}]} withNullExclude(TestInfo): json2 = {"isbn":" 978-4621303252","categories":[]}
整形後。
## json1 { "isbn": " 978-4621303252", "category": { "id": null, "name": "java" }, "categories": [ { "id": null, "name": "java" }, { "id": 2, "name": null } ] } ## json2 { "isbn": " 978-4621303252", "categories": [] }
おや?と思うかもしれませんが、ネストしたプロパティ内のプロパティがnull
だったり、コレクション内のプロパティがnull
だったりすると
そのまま出力されています。
このようなケースに対しては、単にプロパティを保持したクラスに@JsonInclude(JsonInclude.Include.NON_NULL)
を付与しただけでは
効果がありません。
ネストしたクラスにも@JsonInclude(JsonInclude.Include.NON_NULL)を付与する
ネストしたプロパティのnull
も除外するようにしていきましょう。
カテゴリー用のクラスにも@JsonInclude(JsonInclude.Include.NON_NULL)
を付与。
src/main/java/org/littlewings/jackson/nonnull/CategoryNonNull.java
package org.littlewings.jackson.nonnull; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class CategoryNonNull { Integer id; String name; public static CategoryNonNull create(Integer id, String name) { CategoryNonNull category = new CategoryNonNull(); category.setId(id); category.setName(name); return category; } // getter/setterは省略 }
作成したクラスを使うように、書籍側のクラスも作成。
src/main/java/org/littlewings/jackson/nonnull/BookNonNullNested.java
package org.littlewings.jackson.nonnull; import java.util.Arrays; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class BookNonNullNested { String isbn; String title; Integer price; CategoryNonNull category; List<CategoryNonNull> categories; public static BookNonNullNested create(String isbn, String title, Integer price, CategoryNonNull... categories) { BookNonNullNested book = new BookNonNullNested(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); book.setCategory(categories.length > 0 ? categories[0] : null); book.setCategories(categories != null ? Arrays.asList(categories) : null); return book; } // getter/setterは省略 }
テストコード。
@Test void withNullExcludeIncludeNested(TestInfo testInfo) throws JsonProcessingException { BookNonNullNested book1 = BookNonNullNested.create( " 978-4621303252", null, null, CategoryNonNull.create(null, "java"), CategoryNonNull.create(2, null) ); BookNonNullNested book2 = BookNonNullNested.create( " 978-4621303252", null, null ); ObjectMapper mapper = new ObjectMapper(); String json1 = mapper.writeValueAsString(book1); String json2 = mapper.writeValueAsString(book2); System.out.printf("%s: json1 = %s%n", testInfo.getDisplayName(), json1); assertThat(json1) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"category\":{\"name\":\"java\"},\"categories\":[{\"name\":\"java\"},{\"id\":2}]}"); System.out.printf("%s: json2 = %s%n", testInfo.getDisplayName(), json2); assertThat(json2) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"categories\":[]}"); }
標準出力に書き出された内容。
withNullExcludeIncludeNested(TestInfo): json1 = {"isbn":" 978-4621303252","category":{"name":"java"},"categories":[{"name":"java"},{"id":2}]} withNullExcludeIncludeNested(TestInfo): json2 = {"isbn":" 978-4621303252","categories":[]}
整形後。
## json1 { "isbn": " 978-4621303252", "category": { "name": "java" }, "categories": [ { "name": "java" }, { "id": 2 } ] } ## json2 { "isbn": " 978-4621303252", "categories": [] }
これで、ネストしたプロパティからもnull
がなくなりました。
空のコレクションを出力しないようにする
ところで、空のコレクションも出力しないようにする場合はどうしたらいいのでしょうか?
{ "isbn": " 978-4621303252", "categories": [] }
null
の出力抑止と組み合わせると、こんな感じですね。
@JsonInclude( value = JsonInclude.Include.NON_EMPTY, content = JsonInclude.Include.NON_NULL ) public class BookNonNullNested {
特定のプロパティのみ設定する場合はこうなります。
@JsonInclude(
value = JsonInclude.Include.NON_EMPTY,
content = JsonInclude.Include.NON_NULL
)
List<CategoryNonNull> categories;
これは、JsonInclude
アノテーションのJavadocに説明があります。
Note that the main inclusion criteria (one annotated with value()) is checked on Java object level, for the annotated type, and NOT on JSON output -- so even with JsonInclude.Include.NON_NULL it is possible that JSON null values are output, if object reference in question is not
null
. An example is AtomicReference instance constructed to reference null value: such a value would be serialized as JSON null, and not filtered out.To base inclusion on value of contained value(s), you will typically also need to specify content() annotation; for example, specifying only value() as JsonInclude.Include.NON_EMPTY for a {link java.util.Map} would exclude Maps with no values, but would include Maps with
null
values. To exclude Map with onlynull
value, you would use both annotations like so:
public class Bean { @JsonInclude(value=Include.NON_EMPTY, content=Include.NON_NULL) public Map<String,String> entries; }
JsonInclude (Jackson-annotations 2.14.0 API)
設定後の出力結果(整形後)はこうですね。
{ "isbn": " 978-4621303252" }
デフォルトでnullを出力しないように設定する
ここまで@JsonInclude
アノテーションで設定してきましたが、個別に付与するのが面倒だと思うこともあるかなと。
これは、ObjectMapper#setSerializationInclusion
にJsonInclude.Include.NON_NULL
を指定することで実現できます。
@Test void defaultIncludeNonNull(TestInfo testInfo) throws JsonProcessingException { Book book1 = Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); Book book2 = Book.create( " 978-4621303252", null, null ); ObjectMapper mapper = new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL); String json1 = mapper.writeValueAsString(book1); String json2 = mapper.writeValueAsString(book2); System.out.printf("%s: json1 = %s%n", testInfo.getDisplayName(), json1); assertThat(json1) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"category\":{\"name\":\"java\"},\"categories\":[{\"name\":\"java\"},{\"id\":2}]}"); System.out.printf("%s: json2 = %s%n", testInfo.getDisplayName(), json2); assertThat(json2) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"categories\":[]}"); }
先ほどやっていた、空のコレクションの出力を抑制する場合はJsonInclude.Include.NON_EMPTY
を追加します。
@Test void defaultIncludeNonNullAndNonEmpty(TestInfo testInfo) throws JsonProcessingException { Book book1 = Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); Book book2 = Book.create( " 978-4621303252", null, null ); ObjectMapper mapper = new ObjectMapper() .setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_EMPTY); String json1 = mapper.writeValueAsString(book1); String json2 = mapper.writeValueAsString(book2); System.out.printf("%s: json1 = %s%n", testInfo.getDisplayName(), json1); assertThat(json1) .isEqualTo("{\"isbn\":\" 978-4621303252\",\"category\":{\"name\":\"java\"},\"categories\":[{\"name\":\"java\"},{\"id\":2}]}"); System.out.printf("%s: json2 = %s%n", testInfo.getDisplayName(), json2); assertThat(json2) .isEqualTo("{\"isbn\":\" 978-4621303252\"}"); }
ここまで押さえておけばOKかなと。
Spring Bootのプロパティで設定する
次は、Spring Bootのプロパティで設定してみます。ここからは@JsonInclude
アノテーションを個別に付与するのではなく、
ObjectMapper#setSerializationInclusion
をフレームワークの機能で設定する方法を見ていこうと思います。
Spring Bootプロジェクトを作成。
$ curl -s https://start.spring.io/starter.tgz \ -d bootVersion=3.0.1 \ -d javaVersion=17 \ -d type=maven-project \ -d name=spring-boot-jackson-include-non-null \ -d groupId=org.littlewings \ -d artifactId=spring-boot-jackson-include-non-null \ -d version=0.0.1-SNAPSHOT \ -d packageName=org.littlewings.jackson.nonnull \ -d dependencies=web \ -d baseDir=spring-boot-jackson-include-non-null | tar zxvf -
プロジェクト内へ移動。
$ cd spring-boot-jackson-include-non-null
Maven依存関係など。
<properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
ここに、テスト用にREST Assuredを追加しておきました。
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>5.3.0</version> <scope>test</scope> </dependency>
自動生成されたソースコードは、削除しておきます。
$ rm src/main/java/org/littlewings/jackson/nonnull/SpringBootJacksonIncludeNonNullApplication.java src/test/java/org/littlewings/jackson/nonnull/SpringBootJacksonIncludeNonNullApplicationTests.java
このプロジェクトに、先ほどのBook
クラスとCategory
クラスを追加しておきます。
$ cp /path/to/src/main/java/org/littlewings/jackson/nonnull/{Book.java,Category.java} src/main/java/org/littlewings/jackson/nonnull
@JsonInclude
が「付与されていない」方のクラスですね。
public class Book { public class Category {
main
メソッドを持ったクラスに、RestControllerも付けておきます。
src/main/java/org/littlewings/jackson/nonnull/App.java
package org.littlewings.jackson.nonnull; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class App { public static void main(String... args) { SpringApplication.run(App.class, args); } @GetMapping("/simply") public Book simply() { return Book.create( " 978-4621303252", "Effective Java 第3版", 4400, Category.create(1, "java"), Category.create(2, "programming") ); } @GetMapping("/with-null") public Book withNull() { return Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ); } @GetMapping("/with-null-empty-collection") public Book withNullAndEmptyCollection() { return Book.create( " 978-4621303252", null, null ); } }
レスポンスの内容は、Jackson単体でテストしていた時と同じものです。
application.properties
には、以下の内容を定義します。
src/main/resources/application.properties
spring.jackson.default-property-inclusion=non_null
これで、ObjectMapper#serializationInclusion
にJsonInclude.Include
の値を指定していることになります。
Spring Bootのプロパティを見ているとspring.jackson.serialization.*
なのでは?と一瞬思いたくなりますが、こちらは違いますね。
Jackson on/off features that affect the way Java objects are serialized.
Application Properties / JSON Properties / spring.jackson.serialization.*
spring.jackson.default-property-inclusion
の説明をちゃんと見ると、JsonInclude.Include
の値を指定するプロパティであることが書かれています。
Application Properties / JSON Properties / spring.jackson.default-property-inclusion
Controls the inclusion of properties during serialization. Configured with one of the values in Jackson's JsonInclude.Include enumeration.
ソースコード上でも確認。
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion()); }
では、テストコードで確認してみます。
src/test/java/org/littlewings/jackson/nonnull/JacksonTest.java
package org.littlewings.jackson.nonnull; import java.net.URI; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class JacksonTest { @LocalServerPort int port; @Test void simply() { String body = given() .get(URI.create("http://localhost:" + port + "/simply")) .asString(); assertThat(body).isEqualTo("{\"isbn\":\" 978-4621303252\",\"title\":\"Effective Java 第3版\",\"price\":4400,\"category\":{\"id\":1,\"name\":\"java\"},\"categories\":[{\"id\":1,\"name\":\"java\"},{\"id\":2,\"name\":\"programming\"}]}"); } @Test void withNull() { String body = given() .get(URI.create("http://localhost:" + port + "/with-null")) .asString(); assertThat(body).isEqualTo("{\"isbn\":\" 978-4621303252\",\"category\":{\"name\":\"java\"},\"categories\":[{\"name\":\"java\"},{\"id\":2}]}"); } @Test void withNullAndEmptyCollection() { String body = given() .get(URI.create("http://localhost:" + port + "/with-null-empty-collection")) .asString(); assertThat(body).isEqualTo("{\"isbn\":\" 978-4621303252\",\"categories\":[]}"); } }
null
の項目がすべてなくなっています。OKですね。
当然ですが、以下のように設定を削除すると
#spring.jackson.default-property-inclusion=non_null
値がnull
のプロパティも含まれるようになります。
{"isbn":" 978-4621303252","title":null,"price":null,"category":{"id":null,"name":"java"},"categories":[{"id":null,"name":"java"},{"id":2,"name":null}]} {"isbn":" 978-4621303252","title":null,"price":null,"category":null,"categories":[]}
ところで、先ほどJackson単体の時に行っていたJsonInclude.Include
のNON_NULL
とNON_EMPTY
を同時に指定するようなことは、
設定ではできなさそうですね。
この場合はJackson2ObjectMapperBuilderCustomizer
でなんとかするのかな、と思います。
@Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return jackson2ObjectMapperBuilder -> { jackson2ObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_NULL); jackson2ObjectMapperBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY); }; }
Quarkusのプロパティで設定する
最後はQuarkusです。こちらも@JsonInclude
アノテーションを個別に付与するのではなく、ObjectMapper#setSerializationInclusion
を
フレームワークの機能で設定する方法を見ていこうと思います。
Quarkusプロジェクトを作成。
$ mvn io.quarkus.platform:quarkus-maven-plugin:2.15.3.Final:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=quarkus-jackson-include-non-null \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions='resteasy-reactive,resteasy-reactive-jackson' \ -DnoCode
RESTEasyは、なんとなくReactiveにしています。
プロジェクト内に移動。
$ cd quarkus-jackson-include-non-null
Maven依存関係など。
<properties> <compiler-plugin.version>3.10.1</compiler-plugin.version> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id> <quarkus.platform.version>2.15.3.Final</quarkus.platform.version> <skipITs>true</skipITs> <surefire-plugin.version>3.0.0-M7</surefire-plugin.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>${quarkus.platform.group-id}</groupId> <artifactId>${quarkus.platform.artifact-id}</artifactId> <version>${quarkus.platform.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive-jackson</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> </dependencies>
ここにREST Assuredも追加しておきます。
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency>
このプロジェクトに、先ほどのBook
クラスとCategory
クラスを追加しておきます。
$ cp /path/to/src/main/java/org/littlewings/jackson/nonnull/{Book.java,Category.java} src/main/java/org/littlewings/jackson/nonnull
@JsonInclude
が「付与されていない」方のクラスですね。
public class Book { public class Category {
JAX-RSリソースクラスを作成。
src/main/java/org/littlewings/jackson/nonnull/BookResource.java
package org.littlewings.jackson.nonnull; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import io.smallrye.mutiny.Uni; @Path("") public class BookResource { @GET @Path("simply") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> simply() { return Uni .createFrom() .item( Book.create( " 978-4621303252", "Effective Java 第3版", 4400, Category.create(1, "java"), Category.create(2, "programming") ) ); } @GET @Path("with-null") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> withNull() { return Uni .createFrom() .item( Book.create( " 978-4621303252", null, null, Category.create(null, "java"), Category.create(2, null) ) ); } @GET @Path("with-null-empty-collection") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> withNullAndEmptyCollection() { return Uni .createFrom() .item( Book.create( " 978-4621303252", null, null ) ); } }
RESTEasy Reactiveを使っているところ以外は、Spring Bootの時とそう変わりません。
application.properties
には、以下の内容を書いておきます。
src/main/resources/application.properties
quarkus.jackson.serialization-inclusion=non-null
こちらのプロパティですね。
All configuration options / Jackson / quarkus.jackson.serialization-inclusion
Quarkusの場合、指定できる値が書かれているのでわかりやすいかなと思います。
always, non-null, non-absent, non-empty, non-default, custom, use-defaults
テスト。
src/test/java/org/littlewings/jackson/nonnull/BookResourceTest.java
package org.littlewings.jackson.nonnull; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @QuarkusTest @TestHTTPEndpoint(BookResource.class) class BookResourceTest { @Test void simply() { String body = given() .get("/simply") .asString(); assertThat(body, is("{\"isbn\":\" 978-4621303252\",\"title\":\"Effective Java 第3版\",\"price\":4400,\"category\":{\"id\":1,\"name\":\"java\"},\"categories\":[{\"id\":1,\"name\":\"java\"},{\"id\":2,\"name\":\"programming\"}]}")); } @Test void withNull() { String body = given() .get("/with-null") .asString(); assertThat(body, is("{\"isbn\":\" 978-4621303252\",\"category\":{\"name\":\"java\"},\"categories\":[{\"name\":\"java\"},{\"id\":2}]}")); } @Test void withNullAndEmptyCollection() { String body = given() .get("/with-null-empty-collection") .asString(); assertThat(body, is("{\"isbn\":\" 978-4621303252\",\"categories\":[]}")); } }
なお、quarkus.jackson.serialization-inclusion
の指定を削除すると
src/main/resources/application.properties
#quarkus.jackson.serialization-inclusion=non-null
このテストはやはり失敗します。
{\"isbn\":\" 978-4621303252\",\"title\":null,\"price\":null,\"category\":{\"id\":null,\"name\":\"java\"},\"categories\":[{\"id\":null,\"name\":\"java\"},{\"id\":2,\"name\":null}]} {\"isbn\":\" 978-4621303252\",\"title\":null,\"price\":null,\"category\":null,\"categories\":[]}
Quarkusの場合も、JsonInclude.Include
のNON_NULL
とNON_EMPTY
を同時に指定するようなことはプロパティ指定では
できなさそうです。
if (serializationInclusion != null) { objectMapper.setSerializationInclusion(serializationInclusion); }
これを実現したい場合は、ObjectMapperCustomizer
を使えばよさそうです。
こんな感じですね。
src/main/java/org/littlewings/jackson/nonnull/ObjectMapperCustomizerImpl.java
package org.littlewings.jackson.nonnull; import javax.enterprise.context.ApplicationScoped; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.jackson.ObjectMapperCustomizer; @ApplicationScoped public class ObjectMapperCustomizerImpl implements ObjectMapperCustomizer { @Override public void customize(ObjectMapper objectMapper) { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); } }
なんとなく、@Singleton
ではなく@ApplicationScoped
にしておきました…。
Quarkusのテストでも似たようなことをしていました。
@Override public void customize(ObjectMapper objectMapper) { objectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, false) .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false) .configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false) .setSerializationInclusion(JsonInclude.Include.NON_NULL) .setSerializationInclusion(JsonInclude.Include.NON_ABSENT); }
こんなところでしょうか。
まとめ
Jacksonを使ってオブジェクトをシリアライズする際に、null
のプロパティを出力内容に含めない方法を確認していってみました。
Jackson単体、Spring Boot、Quarkusを対象に。
@JsonInclude
での指定はけっこう簡単に見つかるのですが、Spring Bootでの指定がわかりにくい&調べにくい感じがしたので
メモを兼ねてという感じで。
付随的に、いろいろと勉強になりましたが。