こんにちは。松本です。
この記事は TECHSCORE Advent Calendar 2016 の 18 日目の記事です。
6 月頃に書いた記事(「Java : Jackson による JSON デシリアライズ時の型解決方法」)に続いてまた Jackson ネタです。
ここ数年、REST サービスの開発や NoSQL やオブジェクトストレージ利用によって、データシリアライズに JSON を使うことが多くなりました。その分、アプリケーション内で扱うモデルオブジェクトと JSON のマッピングで苦労することも多くなりました。
Jackson はオブジェクトと JSON のマッピング方法が柔軟で多機能なのですが、意外と知られていない機能もあるので、そういったものをピックアップして記事にしようと思います。
@JsonAnyGetter
@JsonAnyGetter アノテーションを使うことで Map 型プロパティのエントリをフラットに並べることができます。
通常、こんなクラスをシリアライズすると extensions プロパティが object 型のプロパティとして表現されます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public class Bean1 {     private String name;     private Map<String, String> extensions = new HashMap<>();     public String getName() {         return name;     }     public void setName(String name) {         this.name = name;     }     public Map<String, String> getExtensions() {         return extensions;     }     public void setExtensions(Map<String, String> extensions) {         this.extensions = extensions;     } } | 
シリアライズしてみます。
| 1 2 3 4 5 6 7 8 | Bean1 bean1 = new Bean1(); bean1.setName("bean"); bean1.getExtensions().put("key1", "value1"); bean1.getExtensions().put("key2", "value2"); ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); String json = objectMapper.writeValueAsString(bean1); System.out.println(json); | 
| 1 2 3 4 5 6 7 | {   "name" : "bean",   "extensions" : {     "key1" : "value1",     "key2" : "value2"   } } | 
クラス定義通り、JSON 上でも extensions プロパティ以下に key1, key2 が並んでいます。
extensions プロパティの Getter に @JsonAnyGetter を付けてシリアライズすると結果が変わります。
| 1 2 3 4 |     @JsonAnyGetter     public Map<String, String> getExtensions() {         return extensions;     } | 
| 1 2 3 4 5 | {   "name" : "bean",   "key1" : "value1",   "key2" : "value2" } | 
先ほどのシリアライズ結果では extensions プロパティ以下に出力された key1, key2 が、name プロパティと同レベルで展開されています。
なお、@JsonAnyGetter アノテーションは Map 型プロパティにしか使えず、ひとつのクラスに複数の @JsonAnyGetter アノテーションをつけることはできません。
@JsonAnySetter
実は上記のように @JsonAnyGetter アノテーションを付けたクラスはデシリアライズに失敗します。
| 1 | Bean1 result = objectMapper.readValue(json, Bean1.class); | 
| 1 2 3 4 5 6 | Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "key1" (class Bean1), not marked as ignorable (2 known properties: "name", "extensions"])  at [Source: {   "name" : "bean",   "key1" : "value1",   "key2" : "value2" }; line: 3, column: 13] (through reference chain: Bean1["key1"]) | 
Bean1 クラスには key1 や key2 というプロパティが存在しないのでデシリアライズできないのです。そこで @JsonAnySetter の登場です。
下記コードのようにプロパティ名と値を受け取るメソッドを追加し、@JsonAnySetter アノテーションを付けます。
| 1 2 3 4 |     @JsonAnySetter     public void setExtension(String key, String value) {         this.extensions.put(key, value);     } | 
こうすることで、存在しないプロパティが全て @JsonAnySetter が付いたメソッドに渡され、処理されるようになります。
@JsonView
@JsonView を使うことでビュー(JSON に出力するプロパティのサブセット)の定義が可能になります。
まずはビューの定義。
| 1 2 3 4 5 6 7 8 9 | public class Views {     public class GroupA {}     public class GroupB extends GroupA {}     public class GroupC {} } | 
ビューを表す 3 つのクラス、GroupA, GroupB, GroupC を定義しています。GroupB は GroupA のサブクラスとして定義している点が本サンプルコードのポイントです。
これらを使ってプロパティのサブセットを定義します。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public class Bean2 {     private String name;     private String type;     private String value;     private String description;     @JsonView(Views.GroupA.class)     public String getName() {         return name;     }     public void setName(String name) {         this.name = name;     }     @JsonView(Views.GroupB.class)     public String getType() {         return type;     }     public void setType(String type) {         this.type = type;     }     public String getValue() {         return value;     }     public void setValue(String value) {         this.value = value;     }     @JsonView(Views.GroupC.class)     public String getDescription() {         return description;     }     public void setDescription(String description) {         this.description = description;     } } | 
Bean2 クラスが持つ 4 つのプロパティのうち、name, type, description の 3 つのプロパティに @JsonView アノテーションを付けました。@JsonView アノテーションにビューを表すクラスを与えることで、対象プロパティがビューのメンバーであることを示しています。
シリアライズしてみます。まずはビューを指定しないケース。
| 1 2 3 4 5 6 7 8 9 | Bean2 bean2 = new Bean2(); bean2.setName("bean2"); bean2.setType("String"); bean2.setValue("ABCDEFG"); bean2.setDescription("hello"); ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); String json1 = objectMapper.writeValueAsString(bean2); System.out.println(json1); | 
| 1 2 3 4 5 6 | {   "name" : "bean2",   "type" : "String",   "value" : "ABCDEFG",   "description" : "hello" } | 
期待通り 4 つのプロパティすべてが出力されました。
次は GroupA を指定してシリアライズ。
| 1 2 | String json2 = objectMapper.writerWithView(Views.GroupA.class).writeValueAsString(bean2); System.out.println(json2); | 
| 1 2 3 4 | {   "name" : "bean2",   "value" : "ABCDEFG" } | 
@JsonView アノテーションで GroupA を指定した name プロパティと、@JsonView アノテーションをつけなかった value プロパティが出力されました。
最後に GroupB を指定してシリアライズします。
| 1 2 | String json3 = objectMapper.writerWithView(Views.GroupB.class).writeValueAsString(bean2); System.out.println(json3); | 
| 1 2 3 4 5 | {   "name" : "bean2",   "type" : "String",   "value" : "ABCDEFG" } | 
ビュー GroupB に含まれるプロパティ type と、@JsonView アノテーションをつけなかった value プロパティに加え、GroupA に含まれるプロパティ name も出力されています。これは、GroupB が GroupA のサブクラスであるためです。
まとめ
JavaBeans の「プロパティ」に対応する JSON の用語は何なのでしょう。
今回の記事ではキャッチーに「プロパティ」と書いてはみましたが、RFC の 4. Objects を見るとこう書かれているので「メンバー」でいいのかな。
An object structure is represented as a pair of curly brackets
surrounding zero or more name/value pairs (or members).

 
						