JavaでJacksonを利用してJSON文字列をコレクション型オブジェクトに変換する

JavaJSONデータ変換をするときに使うJacksonというライブラリのAPIをちゃんと調べたら、ちゃんといい感じの使い方があったのを見つけたのでメモ。

 

 

[Jacksonとは]

Jacksonは主にJSONJavaScript Object Notation)型で表現される文字列をJavaのクラスに変換する、あるいはその逆にJavaのクラスをJSONに変換するためのライブラリである。

github.com

 

このライブラリを利用するには、Mavenを利用しているプロジェクトであればpom.xmlに以下のような記述を追加すれば良い。

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  ...(略)...
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.10.0</version>
    </dependency>
  </dependencies>
</project>

 

JSONとオブジェクトの変換は主にObjectMapperというクラスを通して行う。

jackson-databind/ObjectMapper.java at master · FasterXML/jackson-databind · GitHub

あまり意識したことはなかったが、

* Mapper instances are fully thread-safe as of Jackson 3.0.

とコメントがついていることから、3.X系でようやく完全にスレッドセーフな実装になるようだ。

 

jackson-databind/ObjectMapper.java at 2.11 · FasterXML/jackson-databind · GitHub

こちらのバージョン2.11のコメントを読むと、後から設定を変更しなければ問題はないらしい。なるほど。

* Mapper instances are fully thread-safe provided that ALL configuration of the
* instance occurs before ANY read or write calls. If configuration of a mapper instance
* is modified after first usage, changes may or may not take effect, and configuration
* calls themselves may fail.

 

[文字列をオブジェクトに変換する]

オブジェクトをJSON文字列に、あるいはJSON文字列をオブジェクトに変換するシンプルな例はこのようになる(例外処理は省略)。

java-sandbox/ObjectMapperSample.java at master · kdnakt/java-sandbox · GitHub

final ObjectMapper mapper = new ObjectMapper();
final ObjectWriter writer = mapper.writer();
final Person jack = new Person("Jack", 20);
final String jsonString = writer.writeValueAsString(jack); System.out.println(jsonString);// {"name":"Jack","age":20}
final Person result = mapper.readValue(jsonString, Person.class); System.out.println(result);// Name:Jack, Age:20

 

このとき、Javaのクラス(ここではPersonクラス)のフィールドにはいずれかの方法でアクセスできる必要がある。

  1. 一般的にSetter/Getterメソッドでアクセスできる
  2. フィールドがpublicである

 今回は1の方法を選択した。

 

[コレクション型のオブジェクトに変換する]

Stringやintなどを用いる分には先述のやり方で問題ないのだが、コレクション型のデータをJSONにする場合、一つ困ったことがあった。

final ObjectMapper mapper = new ObjectMapper();
final ObjectWriter writer = mapper.writer();
final Set<String> data = new HashSet<>(); data.add("Hard Rock"); data.add("Heavy Metal");
final String value = writer.writeValueAsString(data); System.out.println(value);// ["Heavy Metal","Hard Rock"]

// WARNING: Type safety: The expression of type Set needs unchecked conversion to conform to Set<String> final Set<String> result = mapper.readValue(value, Set.class); System.out.println(result);

先の例と同じようにSet.classとだけ指定してSet<String>型の変数に変換しようとすると、「Type safety: The expression of type Set needs unchecked conversion to conform to Set<String>」という警告が表示されてしまうのだ。

f:id:kidani_a:20191023033505p:plain

 

あまりじっくり調べる時間がなかったのと、値がこの型以外にはならないことがデータのライフサイクル的に明らかだったので、これまでは以下のように警告を抑制してやり過ごしていた(一番最初はSet<String>.classとか書いてコンパイル通らなくて泣いた)。

@SuppressWarnings("unchecked")
final Set<String> result = mapper.readValue(value, Set.class);

 

ところが、よくよく調べてみると、これまで使用してきたpublic <T> T readValue(String content, Class<T> valueType)以下の2つのメソッドが用意されていることが分かった(全て例外のthrowsは省略)。

  • public <T> T readValue(String content, TypeReference<T> valueTypeRef)
  • public <T> T readValue(String content, JavaType valueType)

 

それぞれ以下のように利用する。

【TypeReferenceを利用する】
final Set<String> result = mapper.readValue(value,
  new TypeReference<Set<String>>() {});
System.out.println(result);// [Heavy Metal, Hard Rock]

【JavaTypeを利用する】
final JavaType type = writer.getTypeFactory()
  .constructCollectionType(Set.class, String.class);
final Set<String> result = mapper.readValue(value, type);
System.out.println(result);// [Heavy Metal, Hard Rock]

 

これで@SuppressWarningsを利用しなくて済むようになる。

 

さらによくよく読んだら、そもそもコレクション型の場合はClass型のメソッドを使うなと書かれていた。それはそうだ。反省……。

* Note: this method should NOT be used if the result type is a
* container ({@link java.util.Collection} or {@link java.util.Map}.
* The reason is that due to type erasure, key and value types
* cannot be introspected when using this method.

 

[まとめ]

  • JavaJSONを扱う場合はJacksonが便利
  • JacksonのObjectMapperは2.X以前は設定変更をするとスレッドセーフでないので注意が必要(3.X系は完全にスレッドセーフ)
  • コレクション型(MapやList、Setなど)をJacksonで扱う場合にはClass型の引数ではなくJavaTypeかTypeReferenceを利用するべし
  • APIドキュメントはよく読もう
  • サンプルコードはGitHubにまとめた

github.com