JavaでJSONデータ変換をするときに使うJacksonというライブラリのAPIをちゃんと調べたら、ちゃんといい感じの使い方があったのを見つけたのでメモ。
[Jacksonとは]
Jacksonは主にJSON(JavaScript Object Notation)型で表現される文字列をJavaのクラスに変換する、あるいはその逆にJavaのクラスをJSONに変換するためのライブラリである。
このライブラリを利用するには、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クラス)のフィールドにはいずれかの方法でアクセスできる必要がある。
- 一般的にSetter/Getterメソッドでアクセスできる
- フィールドが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>」という警告が表示されてしまうのだ。
あまりじっくり調べる時間がなかったのと、値がこの型以外にはならないことがデータのライフサイクル的に明らかだったので、これまでは以下のように警告を抑制してやり過ごしていた(一番最初は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.
[まとめ]
- JavaでJSONを扱う場合はJacksonが便利
- JacksonのObjectMapperは2.X以前は設定変更をするとスレッドセーフでないので注意が必要(3.X系は完全にスレッドセーフ)
- コレクション型(MapやList、Setなど)をJacksonで扱う場合にはClass型の引数ではなくJavaTypeかTypeReferenceを利用するべし
- APIドキュメントはよく読もう
- サンプルコードはGitHubにまとめた