フラットに属性が並んだオブジェクトから、階層のあるJSON への変換(3)

フラットに属性が並んだオブジェクトから、階層のある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"
      }
    }
  }
}