JSONは型が自由
JSONはJavaScriptのオブジェクトっぽい、キーバリューでのデータの表現方法なわけですが、その型には、オブジェクト、配列、文字列、数値、true、false、nullが存在してます。オブジェクトの構造は自由、配列内の型も固定されているわけではないので、以下のようなデータも存在します(良し悪しはおいておいて)。
{
"hoge" : ["string", ["array1", "array2"], {"name" : "三雲修", "code" : "B-2-1"}]
}
これをJacksonというありがたーーーーいライブラリを使用し、いずれにしてもあるクラスにマッピングさせようとした際の備忘録です。
思いつく限りでの解決策
扱う以前にそもそもこの形式のデータに相対したときにいくつかのアプローチが考えられるかと思います。
(1) 全部オブジェクトにしちゃう
{
"hoge" : [{"stringValue" : "string"}, {"arrayValue" : ["array1", "array2"]}, {"name" : "三雲修", "code" : "B-2-1"}]
public class MyJson {
public Hoge hoge;
}
public class Hoge {
public String stringValue;
public List<String> arrayValue;
public String name;
public String code;
}
これならおそらくMyJson
に対してマッピング可能なのではないでしょうか試してないけど。
今回は自分でスキーマを定義していますが、APIなど他者が作ったスキーマの場合もあるのでこの手段がとれないこともあります。
また、JSON側に本質的な意味合いでないキー名をつけなくてはいけない、重複が許されない、という問題もありますね。要するにJSON作る側が手間になるかも、というところ。機械的に生成する上、拡張をあまり考えなくていいのならこれでもいいかもしれません。
(2) Mapにマッピングする
型チェックなどを行った上、自分だけで開発している or 機能が局所化している or ゴリゴリに何が来るかわからないJSONならよさそうです。
(3) JsonNodeのまま扱う
これも(2)と同じ感じで、よりrawに近い感じですかね。
JacksonのAPIについての知識(というほどでもないですが)が要求されるので一手間かかる一方、やれることは増えます。
・・・と、やり口はきっと他にもあるとは思うのですが、今回はPOJOにマッピングします。
JsonDeserializerを使う(マッピング先がListの場合は失敗)
json内の特定のキーの値に応じてであればCreatorを指定する方法でいけそうです。
このJsonはそうではないので、JsonDeserializer
を使って実現します。
@Data
@JsonDeserialize(using = MultiJsonDeserializer.class)
public class Hoge {
// プロパティをそれぞれ用意しておく
// 必須ではないがコンストラクタも用意
private String a;
Hoge(String a) {
this.a = a;
}
private List<String> b;
Hoge(List<String> b) {
this.b = b;
}
private ItemDto c;
Hoge(ItemDto c) {
this.c = c;
}
// Deserializerを継承したクラスを作る
public static class HogeJsonSerializer extends StdDeserializer<Hoge> {
protected HogeJsonDeserializer() {
this(null);
}
protected HogeJsonDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Hoge deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = p.getCodec().readTree(p);
if (node == null) {
return null;
}
Hoge hoge;
// Nodeごとにうまいことアレする
if (node.isArray()) {
List<String> strs = new ArrayList<>();
Iterator<JsonNode> ite = node.iterator();
while(ite.hasNext()) {
JsonNode element = ite.next();
if (element.isTextual()) {
strs.add(element.asText());
}
}
hoge = new Hoge(strs);
} else if (node.isTextual()) {
hoge = new Hoge(node.asText());
} else if (node.isObject()) {
hoge = new Hoge(p.readValueAs(Something.class)); // 別のマッピングを行う
} else {
hoge = null;
}
return hoge;
}
}
}
@Data
public class MyJson {
private List<Hoge> hoge;
}
これでよしーと思ったらうまくいかない。無限ループになってOutOfMemoryErrorになる。
// NG!!!!
@Test
public void test() throws Exception {
final String myJson = "{\"hoge\" : [\"mojiretu\", [\"array1\", \"array2\"], {\"code\" : \"123\", \"name\" : \"myName\"}]}";
ObjectMapper om = new ObjectMapper().readerFor(Hoge.class).readValue(myJson);
}
Listではなく、単位値であればうまくいきました。
@Data
public class MyJson {
private Hoge hoge;
}
@Test
public void test() throws Exception {
String myJson = "{\"hoge\" : \"mojiretsu\"}"; // 文字列
ObjectMapper om = new ObjectMapper().readerFor(Hoge.class).readValue(myJson);
myJson = "{\"hoge\" : [\"array1\", \"array2\"]}"; // 配列
om = new ObjectMapper().readerFor(Hoge.class).readValue(myJson);
myJson = "{\"hoge\" : {\"name\" : \"三雲修\", \"code\" : \"B-2-1\"]}"; // オブジェクト
om = new ObjectMapper().readerFor(Hoge.class).readValue(myJson);
}
Listに対してマッピングする場合
アノテーションつけたらうまくいきました。よかった
@Data
public class MyJson {
@JsonDeserialize(contentUsing = HogeJsonDeserializer.class) // これつけたらいけた
private List<Hoge> hoge;
}