简体   繁体   中英

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

I've seen that the default TypeAdapter for Enum doesn't fit my need:

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 the Enum has value ONE and TWO, when we try to parse THREE, then this value is unknown and Gson will map null instead of raising a parsing exception. I need something more fail-fast.

But I also need something which permits me to know the name of the field which is currently read and creates a parsing failure.

Is it possible with Gson?

Yes .

Gson is quite modular to allow you to use your own TypeAdapterFactory for the enum case. Your custom adapter will return your own EnumTypeAdapter and manage the wanted case. Let the code speak.

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 runtime exception:

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 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 stacktrace:

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 does not know anything about the context it's called, so this solution is not enough to catch the field name.

Edit

So you have to use also another TypeAdapter that I called CustomReflectiveTypeAdapterFactory and is almost a copy of CustomReflectiveTypeAdapterFactory and I changed a bit the exception, 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's 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 read method where I catch the exception and add the field name and throw again exception. Note that class CustomTypeAdapterRuntimeTypeWrapper is simply a renamed copy of TypeAdapterRuntimeTypeWrapper in library internals since class is private.

So, 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 (changes to 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 comes at some costs.

  • First off all, you have to copy some private/final classes and do your changes. If library get updated, you have to check again your code (a fork of source code would be the same, but at least you do not have to copy all that code).
  • If you customize field exclusion strategy, constructors or field naming policies you have to replicate them into the CustomReflectiveTypeAdapterFactory since I do not find any possibility to pass them from the builder.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM