package fr.pandacube.lib.core.util; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.RecordComponent; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; public class Json { public static final Gson gson = build(b -> b); public static final Gson gsonPrettyPrinting = build(b -> b.setPrettyPrinting()); public static final Gson gsonSerializeNulls = build(b -> b.serializeNulls()); public static final Gson gsonSerializeNullsPrettyPrinting = build(b -> b.serializeNulls().setPrettyPrinting()); private static Gson build(Function builderModifier) { return builderModifier .apply(new GsonBuilder().registerTypeAdapterFactory(new RecordAdapterFactory()).setLenient()).create(); } // from https://github.com/google/gson/issues/1794#issuecomment-812964421 private static class RecordAdapterFactory implements TypeAdapterFactory { @Override public TypeAdapter create(Gson gson, TypeToken type) { @SuppressWarnings("unchecked") Class clazz = (Class) type.getRawType(); if (!clazz.isRecord() || clazz == Record.class) { return null; } return new RecordTypeAdapter<>(gson, this, type); } } private static class RecordTypeAdapter extends TypeAdapter { private Gson gson; private TypeAdapterFactory factory; private TypeToken type; public RecordTypeAdapter(Gson gson, TypeAdapterFactory factory, TypeToken type) { this.gson = gson; this.factory = factory; this.type = type; } @Override public void write(JsonWriter out, T value) throws IOException { gson.getDelegateAdapter(factory, type).write(out, value); } @Override public T read(JsonReader reader) throws IOException { if (reader.peek() == JsonToken.NULL) { reader.nextNull(); return null; } else { @SuppressWarnings("unchecked") Class clazz = (Class) type.getRawType(); RecordComponent[] recordComponents = clazz.getRecordComponents(); Map> typeMap = new HashMap<>(); for (int i = 0; i < recordComponents.length; i++) { typeMap.put(recordComponents[i].getName(), TypeToken.get(recordComponents[i].getGenericType())); } var argsMap = new HashMap(); reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); argsMap.put(name, gson.getAdapter(typeMap.get(name)).read(reader)); } reader.endObject(); var argTypes = new Class[recordComponents.length]; var args = new Object[recordComponents.length]; for (int i = 0; i < recordComponents.length; i++) { argTypes[i] = recordComponents[i].getType(); args[i] = argsMap.get(recordComponents[i].getName()); } Constructor constructor; try { constructor = clazz.getDeclaredConstructor(argTypes); constructor.setAccessible(true); return constructor.newInstance(args); } catch (NoSuchMethodException | InstantiationException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } } } } }