简体   繁体   English

Gson:我们可以在类型适配器中获取序列化字段名称吗?

[英]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: 我已经看到Enum的默认TypeAdapter不符合我的需要:

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. 如果Enum的值为ONE和TWO,当我们尝试解析THREE时,则该值未知,Gson将映射null而不是引发解析异常。 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? Gson有可能吗?

Yes . 是的

Gson is quite modular to allow you to use your own TypeAdapterFactory for the enum case. Gson非常模块化,允许您使用自己的TypeAdapterFactory作为枚举案例。 Your custom adapter will return your own EnumTypeAdapter and manage the wanted case. 您的自定义适配器将返回您自己的EnumTypeAdapter并管理所需的案例。 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. 不幸的是,EnumTypeAdapter对它所调用的上下文一无所知,所以这个解决方案不足以捕获字段名称。

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: 所以你必须使用我称为CustomReflectiveTypeAdapterFactory另一个TypeAdapter ,它几乎是CustomReflectiveTypeAdapterFactory的副本,我改变了一点异常,所以:

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. 最重要的部分是read方法,我捕获异常并添加字段名称并再次抛出异常。 Note that class CustomTypeAdapterRuntimeTypeWrapper is simply a renamed copy of TypeAdapterRuntimeTypeWrapper in library internals since class is private. 请注意,类CustomTypeAdapterRuntimeTypeWrapper只是库内部的TypeAdapterRuntimeTypeWrapper的重命名副本,因为类是私有的。

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. 如果您自定义字段排除策略,构造函数或字段命名策略,则必须将它们复制到CustomReflectiveTypeAdapterFactory因为我没有发现从构建器传递它们的任何可能性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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