フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(2) - Oboe吹きプログラマの黙示録
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
これは、1階層のグルーピングしかサポートしない。
もっと階層が深い場合は、同じ処理ロジックのシリアライザではだめだ。
そこで、もっと階層の指定が深くできるように、シリアライズ後に想定するJSONの
JSON-PATH を指定してあらゆる階層でもフラットからツリーに変換できるように考えた。
フィールドに付与するアノテーション
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * JSON パスをフィールドアノテーションで書く */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.FIELD}) public @interface JsonPath { String value(); }
このアノテーションが付いた場合、階層化する JsonSerializer
import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.StringTokenizer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import com.google.gson.annotations.SerializedName; /** * AutoPathSerializer */ public class AutoPathSerializer<T> implements JsonSerializer<T>{ @Override public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context){ Map<String, JsonObject> jmap = new HashMap<>(); JsonObject jo = new JsonObject(); jmap.put("$", jo); Field[] fields = src.getClass().getDeclaredFields(); for(Field f:fields) { try{ f.setAccessible(true); Object obj = f.get(src); JsonPath jpath = f.getAnnotation(JsonPath.class); if (jpath != null) { List<String> plist = tokenToList(jpath.value(), '.', '\\'); String path = plist.remove(0); for(String t:plist){ JsonObject parent = jmap.get(path); path = path + "." + t; JsonObject jt = jmap.get(path); if (jt==null) { jmap.put(path, new JsonObject()); } jt = jmap.get(path); parent.add(t, jt); } JsonObject addjo = jmap.get(path); if (obj instanceof String) { addjo.addProperty(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName()) , Optional.ofNullable(obj).map(e->e.toString()).orElse(null)); }else { addjo.add(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName()) , context.serialize(obj, f.getType())); } jmap.put(path, addjo); }else{ if (obj instanceof String) { jo.addProperty(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName()) , Optional.ofNullable(obj).map(e->e.toString()).orElse(null)); }else { jo.add(Optional.ofNullable(f.getAnnotation(SerializedName.class)).map(e->e.value()).orElse(f.getName()) , context.serialize(obj, f.getType())); } } }catch(IllegalAccessException e){ throw new RuntimeException(e); } } return jo; } private List<String> tokenToList(String str, char sep, char escape){ List<String> list = new ArrayList<>(); String sp = new String(new char[]{ sep }); String escapes = new String(new char[]{ escape, escape }); StringTokenizer st = new StringTokenizer(str, sp, true); String s = ""; while (st.hasMoreTokens()){ String c = st.nextToken(); if (c.equals(sp)) { if (s.charAt(s.length()-1)==escape) { s += c; }else{ list.add(s.replaceAll(escapes, "")); s = ""; } }else { s += c; } } list.add(s.replaceAll(escapes, "")); return list; } }
使用例
アノテーションを付けた対象クラス
public class Datasample{ public String a1; @JsonPath("$.aaa") public double a2; @JsonPath("$.aaa") public boolean a3; public String b1; @JsonPath("$.bbb") public int b2; @JsonPath("$.bbb.BBB") public boolean b3; @JsonPath("$.bbb.BBB") public LocalDate b4; @JsonPath("$.bbb.BBB") public String b5; public int c1; @JsonPath("$.ccc.CCC") public String c2; @JsonPath("$.ccc.CCC") public String c3"; @JsonPath("$.ccc.CC10") public String c4"; @JsonPath("$.ccc.CC10") public String c5; @JsonPath("$.ccc.CC20") public String c6; @JsonPath("$.ccc.CC20.cc33") public String c7; }
AutoPathSerializer 指定
Gson gson = new GsonBuilder() .serializeNulls() .registerTypeAdapter(LocalDate.class, LocalDateAdapter.of("yyyy/MM/dd")) .registerTypeAdapter(Datasample.class, new AutoPathSerializer<Datasample>()) .setPrettyPrinting() .create();
シリアライズ結果例 JSON
{ "a1": "A1", "aaa": { "a2": 13.09, "a3": true }, "b1": "B1", "bbb": { "b2": 21, "BBB": { "b3": true, "b4": "2020/07/02", "b5": "B5" } }, "c1": 323, "ccc": { "CCC": { "c2": "C2", "c3": "C3" }, "CC10": { "c4": "C4", "c5": "C5" }, "CC20": { "c6": "C6", "cc33": { "c7": "C7" } } } }