これまで、JSONといえばJSONICを使っていたのですが、ちょっとしたことからJacksonを使ってみたので、これを機に書いておくことにしました。
FasterXML/Jackson
https://github.com/FasterXML/jackson
JSON関連では相当有名なライブラリですが、仕事ではSeasar2を使うことが多いのでその組み合わせでJSONICを使っていたんですよね。
今回は速度を求めたいので、Jacksonというわけです。
まずは使ってみる
Jacksonを使う時には、StreamingとDatabindがあるようですが、今回はDatabindを使用します。
Jackson Databind
https://github.com/FasterXML/jackson-databind
Maven依存関係。
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.2.3</version> </dependency>
これで、一緒にCoreとAnnotationsも引っ張られてきます。
では、JSONとオブジェクトのマッピング対象としてこんなクラスを用意します。
src/main/java/Person.java import java.util.*; import java.util.concurrent.*; public class Person { private String name; private int age; private List<Person> friends = new ArrayList<>(); public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void addFriend(Person person) { friends.add(person); } public List<Person> getFriends() { return friends; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Class[").append(getClass().getName()).append("] "); builder.append("name = ").append(name).append(", "); builder.append("age = ").append(Integer.toString(age)).append(" "); builder.append("friends[").append(friends.getClass().getName()).append("] = ").append(friends.toString()); return builder.toString(); } }
わざわざtoStringをオーバーライドしているのは…まあ、後で。
では、使ってみましょう。まずは、オブジェクトからJSONへ。
Person katsuo = new Person("磯野カツオ", 11); katsuo.addFriend(new Person("中島弘", 10)); katsuo.addFriend(new Person("花沢花子", 11)); katsuo.addFriend(new Person("大空カオリ", 10));
オブジェクトを定義して…あ、年齢は適当です(笑)。ObjectMapperのインスタンスを作成して、writeValueすればOKです。
try (FileOutputStream fos = new FileOutputStream(FILE_NAME); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); BufferedWriter writer = new BufferedWriter(osw)) { ObjectMapper mapper = new ObjectMapper(); mapper.writeValue(writer, katsuo); } catch (IOException e) { e.printStackTrace(); }
writeValueの引数にはOutputStreamだったりFileが取れたり、writeValueAsStringやwriteValueAsBytesのような変換後の結果を受け取るメソッドもあります。
出力結果は、こんな感じ。
{"name":"磯野カツオ","age":11,"friends":[{"name":"中島弘","age":10,"friends":[]},{"name":"花沢花子","age":11,"friends":[]},{"name":"大空カオリ","age":10,"friends":[]}]}
JSONですねー。
JSONからオブジェクトに戻す時は、ObjectMapper#readValueを使用します。
try (FileInputStream fis = new FileInputStream(FILE_NAME); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { ObjectMapper mapper = new ObjectMapper(); System.out.println(mapper.readValue(reader, Person.class)); } catch (IOException e) { e.printStackTrace(); }
readValueメソッドの引数には、他にInputStreamやFile、Stringやbyte配列などが取れたりします。また、JSONからオブジェクトに戻した時のトップレベルの型が必要です。
オブジェクトに戻した時の出力結果は、toStringをオーバーライドしているのでこんな感じで確認できます。
Class[Person] name = 磯野カツオ, age = 11 friends[java.util.ArrayList] = [Class[Person] name = 中島弘, age = 10 friends[java.util.ArrayList] = [], Class[Person] name = 花沢花子, age = 11 friends[java.util.ArrayList] = [], Class[Person] name = 大空カオリ, age = 10 friends[java.util.ArrayList] = []]
すごい簡単に使えますね!!
JSON出力時にPrettyPrintしたい
デフォルトでは、スペースや改行のないJSONが出力されるので、データとして扱う時はそれで問題ありませんが、人見る時にはちょっとつらいものです。
というわけで、PrettyPrintしましょう。
ObjectMapper#enableで、SerializationFeature.INDENT_OUTPUTを有効にします。
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.writeValue(writer, katsuo);
すると、出力されるJSONが
{ "name" : "磯野カツオ", "age" : 11, "friends" : [ { "name" : "中島弘", "age" : 10, "friends" : [ ] }, { "name" : "花沢花子", "age" : 11, "friends" : [ ] }, { "name" : "大空カオリ", "age" : 10, "friends" : [ ] } ] }
という形になります。
privateフィールドも対象にしよう
例えば、Personクラスにnicknameというprivateフィールドを追加します。
public class Person { private String name; private int age; private List<Person> friends = new ArrayList<>(); private String nickname; public Person() { } public Person(String name, int age, String nickname) { this.name = name; this.age = age; this.nickname = nickname; }
そして、保存側のコードをこう変えて
Person katsuo = new Person("磯野カツオ", 11, "カツオ"); katsuo.addFriend(new Person("中島弘", 10, "中島")); katsuo.addFriend(new Person("花沢花子", 11, "花沢さん")); katsuo.addFriend(new Person("大空カオリ", 10, "カオリちゃん"));
できたJSONを見ると、nicknameが無視されています。
{ "name" : "磯野カツオ", "age" : 11, "friends" : [ { "name" : "中島弘", "age" : 10, "friends" : [ ] }, { "name" : "花沢花子", "age" : 11, "friends" : [ ] }, { "name" : "大空カオリ", "age" : 10, "friends" : [ ] } ] }
そこで、可視性の変更を行います。保存を行うコード、読み込みを行うコードそれぞれに、ObjectMapper#setVibilityで設定を行います。こちらは、保存時。
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.writeValue(writer, katsuo);
こちらは、読み込み時。
ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); System.out.println(mapper.readValue(reader, Person.class));
第1引数がJSON変換を行う対象で、第2引数が可視性の範囲です。ANYなので、privateからpublicすべてが対象になります。
というわけで、今度は保存されました。
{ "name" : "磯野カツオ", "age" : 11, "friends" : [ { "name" : "中島弘", "age" : 10, "friends" : [ ], "nickname" : "中島" }, { "name" : "花沢花子", "age" : 11, "friends" : [ ], "nickname" : "花沢さん" }, { "name" : "大空カオリ", "age" : 10, "friends" : [ ], "nickname" : "カオリちゃん" } ], "nickname" : "カツオ" }
読み込み時の設定を忘れると、未知のプロパティとして扱われてエラーとなってしまいます。それを回避する方法もありますが。
JSONに型情報を埋め込む
実は、今回JSONをJavaのシリアライズの代替の選択肢となるかどうかを検討していて、その時にひっかかったのがここでした。
例えば、Personのfriendsフィールドを意味もなくConcurrentHashMapにしてみます。
private Map<String, Person> friends = new ConcurrentHashMap<>();
そして、JSON化して
{ "name" : "磯野カツオ", "age" : 11, "friends" : { "花沢花子" : { "name" : "花沢花子", "age" : 11, "friends" : { }, "nickname" : "花沢さん" }, "大空カオリ" : { "name" : "大空カオリ", "age" : 10, "friends" : { }, "nickname" : "カオリちゃん" }, "中島弘" : { "name" : "中島弘", "age" : 10, "friends" : { }, "nickname" : "中島" } }, "nickname" : "カツオ" }
Javaオブジェクトに戻すと…
Class[Person] name = 磯野カツオ, age = 11 nickname = カツオ friends[java.util.LinkedHashMap] = {花沢花子=Class[Person] name = 花沢花子, age = 11 nickname = 花沢さん friends[java.util.LinkedHashMap] = {}, 大空カオリ=Class[Person] name = 大空カオリ, age = 10 nickname = カオリちゃん friends[java.util.LinkedHashMap] = {}, 中島弘=Class[Person] name = 中島弘, age = 10 nickname = 中島 friends[java.util.LinkedHashMap] = {}}
LinkedHashMapになってしまいました!!
さらに、friendsフィールドを
private Map<String, Object> friends = new ConcurrentHashMap<>();
のようにド汎用的な型に変えてしまうと、friendsフィールドの中身はPersonクラスにすら戻せなくなります。
Class[Person] name = 磯野カツオ, age = 11 nickname = カツオ friends[java.util.LinkedHashMap] = {花沢花子={name=花沢花子, age=11, friends={}, nickname=花沢さん}, 大空カオリ={name=大空カオリ, age=10, friends={}, nickname=カオリちゃん}, 中島弘={name=中島弘, age=10, friends={}, nickname=中島}}
まあ、JSONなんだからそりゃあそうだよねぇと思いつつ、このようなポリモーフィックな型が情報が保存できるような対応も、一応あるみたいです。
まずは、保存時のコードにObjectMapper#enableDefaultTypingの呼び出しを加えます。
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping();
mapper.writeValue(writer, katsuo);
enableDefaultTypingメソッドには引数あり版があるので、またカスタマイズが効くみたいですね。
出力されるJSONには、型情報が入るようになります。トップレベルは、readValue時に指定すればいいので入らないってことなのでしょうね。
{ "name" : "磯野カツオ", "age" : 11, "friends" : [ "java.util.concurrent.ConcurrentHashMap", { "花沢花子" : [ "Person", { "name" : "花沢花子", "age" : 11, "friends" : [ "java.util.concurrent.ConcurrentHashMap", { } ], "nickname" : "花沢さん" } ], "大空カオリ" : [ "Person", { "name" : "大空カオリ", "age" : 10, "friends" : [ "java.util.concurrent.ConcurrentHashMap", { } ], "nickname" : "カオリちゃん" } ], "中島弘" : [ "Person", { "name" : "中島弘", "age" : 10, "friends" : [ "java.util.concurrent.ConcurrentHashMap", { } ], "nickname" : "中島" } ] } ], "nickname" : "カツオ" }
そして、読み込み時もenableDefaultTypingを有効にするように変更します。
ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(); System.out.println(mapper.readValue(reader, Person.class));
すると、型情報まで含めて戻せるようになります。
Class[Person] name = 磯野カツオ, age = 11 nickname = カツオ friends[java.util.concurrent.ConcurrentHashMap] = {花沢花子=Class[Person] name = 花沢花子, age = 11 nickname = 花沢さん friends[java.util.concurrent.ConcurrentHashMap] = {}, 大空カオリ=Class[Person] name = 大空カオリ, age = 10 nickname = カオリちゃん friends[java.util.concurrent.ConcurrentHashMap] = {}, 中島弘=Class[Person] name = 中島弘, age = 10 nickname = 中島 friends[java.util.concurrent.ConcurrentHashMap] = {}}
まあ、サイズも大きくなるし、言語間の可搬性もなくなるのでそんなに使わないとは思いますが…。
その他、Mapper、Serialization、Deserializationなど設定がいろいろできるので、ご参考に。
Mapper Features
https://github.com/FasterXML/jackson-databind/wiki/Mapper-Features
Serialization features
https://github.com/FasterXML/jackson-databind/wiki/Serialization-Features
Deserialization features
https://github.com/FasterXML/jackson-databind/wiki/Deserialization-Features
そんなところで。
src/main/java/Person.java
import java.util.*; import java.util.concurrent.*; public class Person { private String name; private int age; private Map<String, Object> friends = new ConcurrentHashMap<>(); //private List<Person> friends = new ArrayList<>(); private String nickname; public Person() { } public Person(String name, int age, String nickname) { this.name = name; this.age = age; this.nickname = nickname; } public void setName(String name) { this.name = name; } public String getName() { return name; } public void setAge(int age) { this.age = age; } public int getAge() { return age; } public void addFriend(Person person) { friends.put(person.getName(), person); //friends.add(person); } public Person getFriend(String name) { return (Person) friends.get(name); } /* public List<Person> getFriends() { return friends; } */ @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Class[").append(getClass().getName()).append("] "); builder.append("name = ").append(name).append(", "); builder.append("age = ").append(Integer.toString(age)).append(" "); builder.append("nickname = ").append(nickname).append(" "); builder.append("friends[").append(friends.getClass().getName()).append("] = ").append(friends.toString()); return builder.toString(); } }
src/main/java/JacksonExample.java
import java.io.*; import java.nio.charset.StandardCharsets; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; public class JacksonExample { private static final String FILE_NAME = "person.json"; public static void main(String[] args) { if (args.length == 0) { System.out.println("Require Argument: ser or des"); System.exit(1); } switch(args[0]) { case "ser": toJson(); break; case "des": toObject(); break; default: System.out.println("Require Argument: ser or des"); System.exit(1); } } public static void toJson() { Person katsuo = new Person("磯野カツオ", 11, "カツオ"); katsuo.addFriend(new Person("中島弘", 10, "中島")); katsuo.addFriend(new Person("花沢花子", 11, "花沢さん")); katsuo.addFriend(new Person("大空カオリ", 10, "カオリちゃん")); try (FileOutputStream fos = new FileOutputStream(FILE_NAME); OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); BufferedWriter writer = new BufferedWriter(osw)) { ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(); mapper.writeValue(writer, katsuo); } catch (IOException e) { e.printStackTrace(); } } public static void toObject() { try (FileInputStream fis = new FileInputStream(FILE_NAME); InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8); BufferedReader reader = new BufferedReader(isr)) { ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(); System.out.println(mapper.readValue(reader, Person.class)); } catch (IOException e) { e.printStackTrace(); } } }