Gson: can we get the serialized field name in the type adapter?

I saw that the default TypeAdapter for Enum does not suit me:

private static final class EnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> { private final Map<String, T> nameToConstant = new HashMap<String, T>(); private final Map<T, String> constantToName = new HashMap<T, String>(); public EnumTypeAdapter(Class<T> classOfT) { try { for (T constant : classOfT.getEnumConstants()) { String name = constant.name(); SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class); if (annotation != null) { name = annotation.value(); } nameToConstant.put(name, constant); constantToName.put(constant, name); } } catch (NoSuchFieldException e) { throw new AssertionError(); } } public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } return nameToConstant.get(in.nextString()); } public void write(JsonWriter out, T value) throws IOException { out.value(value == null ? null : constantToName.get(value)); } } 

If Enum is ONE and TWO when we try to parse THREE, this value is unknown, and Gson will display zero instead of raising the parsing exception. I need something more unsuccessful.

But I also need something that allows me to find out the name of the field that is currently being read, and creates parsing.

Is this possible with Gson?

+4
source share
1 answer

Yes

Gson is modular enough so you can use your own TypeAdapterFactory for an enumeration case. Your custom adapter will return your own EnumTypeAdapter and manage the desired case. Let the code say.

 package stackoverflow.questions.q16715117; import java.io.IOException; import java.util.*; import com.google.gson.*; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.*; public class Q16715117 { public static void main(String[] args) { GsonBuilder gb = new GsonBuilder(); gb.registerTypeAdapterFactory(CUSTOM_ENUM_FACTORY); Container c1 = new Container(); Gson g = gb.create(); String s1 = "{\"colour\":\"RED\",\"number\":42}"; c1 = g.fromJson(s1, Container.class); System.out.println("Result: "+ c1.toString()); } public static final TypeAdapterFactory CUSTOM_ENUM_FACTORY = newEnumTypeHierarchyFactory(); public static TypeAdapterFactory newEnumTypeHierarchyFactory() { return new TypeAdapterFactory() { @SuppressWarnings({"rawtypes", "unchecked"}) public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) { Class<? super T> rawType = typeToken.getRawType(); if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) { return null; } if (!rawType.isEnum()) { rawType = rawType.getSuperclass(); // handle anonymous subclasses } return (TypeAdapter<T>) new CustomEnumTypeAdapter(rawType); } }; } private static final class CustomEnumTypeAdapter<T extends Enum<T>> extends TypeAdapter<T> { private final Map<String, T> nameToConstant = new HashMap<String, T>(); private final Map<T, String> constantToName = new HashMap<T, String>(); private Class<T> classOfT; public CustomEnumTypeAdapter(Class<T> classOfT) { this.classOfT = classOfT; try { for (T constant : classOfT.getEnumConstants()) { String name = constant.name(); SerializedName annotation = classOfT.getField(name).getAnnotation(SerializedName.class); if (annotation != null) { name = annotation.value(); } nameToConstant.put(name, constant); constantToName.put(constant, name); } } catch (NoSuchFieldException e) { throw new AssertionError(); } } public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } String nextString = in.nextString(); T enumValue = nameToConstant.get(nextString); if (enumValue == null) throw new GsonEnumParsinException(nextString, classOfT.getName()); return enumValue; } public void write(JsonWriter out, T value) throws IOException { out.value(value == null ? null : constantToName.get(value)); } } } 

Plus, I declared a custom exception to execute:

 public class GsonEnumParsinException extends RuntimeException { String notFoundEnumValue; String enumName; String fieldName; public GsonEnumParsinException(String notFoundEnumValue, String enumName) { this.notFoundEnumValue = notFoundEnumValue; this.enumName = enumName; } @Override public String toString() { return "GsonEnumParsinException [notFoundEnumValue=" + notFoundEnumValue + ", enumName=" + enumName + "]"; } public String getNotFoundEnumValue() { return notFoundEnumValue; } @Override public String getMessage() { return "Cannot found " + notFoundEnumValue + " for enum " + enumName; } } 

These are the classes that I used in the example:

 public enum Colour { WHITE, YELLOW, BLACK; } public class Container { private Colour colour; private int number; public Colour getColour() { return colour; } public void setColour(Colour colour) { this.colour = colour; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } @Override public String toString() { return "Container [colour=" + colour + ", number=" + number + "]"; } } 

This gives this stack:

 Exception in thread "main" GsonEnumParsinException [notFoundEnumValue=RED, enumName=stackoverflow.questions.q16715117.Colour] at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:77) at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:1) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:93) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:172) at com.google.gson.Gson.fromJson(Gson.java:803) at com.google.gson.Gson.fromJson(Gson.java:768) at com.google.gson.Gson.fromJson(Gson.java:717) at com.google.gson.Gson.fromJson(Gson.java:689) at stackoverflow.questions.q16715117.Q16715117.main(Q16715117.java:22) 

Unfortunately, the EnumTypeAdapter knows nothing about the context it is calling, so this solution is not enough to catch the name of the field.

Edit

So, you need to use another TypeAdapter , which I called CustomReflectiveTypeAdapterFactory , and this is almost a copy of CustomReflectiveTypeAdapterFactory , and I changed this exception a bit, so:

 public final class CustomReflectiveTypeAdapterFactory implements TypeAdapterFactory { private final ConstructorConstructor constructorConstructor; private final FieldNamingStrategy fieldNamingPolicy; private final Excluder excluder; public CustomReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor, FieldNamingStrategy fieldNamingPolicy, Excluder excluder) { this.constructorConstructor = constructorConstructor; this.fieldNamingPolicy = fieldNamingPolicy; this.excluder = excluder; } public boolean excludeField(Field f, boolean serialize) { return !excluder.excludeClass(f.getType(), serialize) && !excluder.excludeField(f, serialize); } private String getFieldName(Field f) { SerializedName serializedName = f.getAnnotation(SerializedName.class); return serializedName == null ? fieldNamingPolicy.translateName(f) : serializedName.value(); } public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { Class<? super T> raw = type.getRawType(); if (!Object.class.isAssignableFrom(raw)) { return null; // it a primitive! } ObjectConstructor<T> constructor = constructorConstructor.get(type); return new Adapter<T>(constructor, getBoundFields(gson, type, raw)); } private CustomReflectiveTypeAdapterFactory.BoundField createBoundField( final Gson context, final Field field, final String name, final TypeToken<?> fieldType, boolean serialize, boolean deserialize) { final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); // special casing primitives here saves ~5% on Android... return new CustomReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) { final TypeAdapter<?> typeAdapter = context.getAdapter(fieldType); @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree @Override void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException { Object fieldValue = field.get(value); TypeAdapter t = new CustomTypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType()); t.write(writer, fieldValue); } @Override void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { Object fieldValue = null; try { fieldValue = typeAdapter.read(reader); } catch (GsonEnumParsinException e){ e.setFieldName(field.getName()); throw e; } if (fieldValue != null || !isPrimitive) { field.set(value, fieldValue); } } }; } // more copy&paste code follows 

The most important part is the read method, where I catch an exception and add a field name and throw an exception. Note that the CustomTypeAdapterRuntimeTypeWrapper class is simply a renamed copy of TypeAdapterRuntimeTypeWrapper in internal libraries, since the class is private.

So, the main method changes as follows:

  Map<Type, InstanceCreator<?>> instanceCreators = new HashMap<Type, InstanceCreator<?>>(); Excluder excluder = Excluder.DEFAULT; FieldNamingStrategy fieldNamingPolicy = FieldNamingPolicy.IDENTITY; GsonBuilder gb = new GsonBuilder(); gb.registerTypeAdapterFactory(new CustomReflectiveTypeAdapterFactory(new ConstructorConstructor(instanceCreators), fieldNamingPolicy, excluder)); gb.registerTypeAdapterFactory(CUSTOM_ENUM_FACTORY); Gson g = gb.create(); 

and now you have this stacktrace (the changes to the exception are so simple that I omitted them):

 Exception in thread "main" GsonEnumParsinException [notFoundEnumValue=RED, enumName=stackoverflow.questions.q16715117.Colour, fieldName=colour] at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:90) at stackoverflow.questions.q16715117.Q16715117$CustomEnumTypeAdapter.read(Q16715117.java:1) at stackoverflow.questions.q16715117.CustomReflectiveTypeAdapterFactory$1.read(CustomReflectiveTypeAdapterFactory.java:79) at stackoverflow.questions.q16715117.CustomReflectiveTypeAdapterFactory$Adapter.read(CustomReflectiveTypeAdapterFactory.java:162) at com.google.gson.Gson.fromJson(Gson.java:803) at com.google.gson.Gson.fromJson(Gson.java:768) at com.google.gson.Gson.fromJson(Gson.java:717) at com.google.gson.Gson.fromJson(Gson.java:689) at stackoverflow.questions.q16715117.Q16715117.main(Q16715117.java:35) 

Of course, this solution brings some costs.

  • First of all, you need to copy some private / final classes and make your changes. If the library is updated, you need to check your code again (the fork of the source code will be the same, but at least you do not need to copy all this code).
  • If you are configuring a field exclusion strategy, constructors, or field naming policies, you must replicate them in the CustomReflectiveTypeAdapterFactory , since I do not see the ability to pass them from the constructor.
+4
source

Source: https://habr.com/ru/post/1482396/


All Articles