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

时间:2013-05-23 13:23:44

标签: java json parsing gson

我已经看到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));
    }
  }

如果Enum的值为ONE和TWO,当我们尝试解析THREE时,则该值未知,Gson将映射null而不是引发解析异常。我需要更快失败的东西。

但是我还需要能够让我知道当前读取的字段名称并创建解析失败的东西。

Gson可以吗?

1 个答案:

答案 0 :(得分:5)

Gson非常模块化,允许您使用自己的TypeAdapterFactory作为枚举案例。您的自定义适配器将返回您自己的EnumTypeAdapter并管理所需的案例。让代码说话。

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));
        }
      }

}

另外,我宣布了一个自定义运行时异常:

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;
    }


}

这些是我在示例中使用的类:

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 + "]";
    }

}

这给出了这个堆栈跟踪:

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)

不幸的是,EnumTypeAdapter对它所调用的上下文一无所知,所以这个解决方案不足以捕获字段名称。

修改

所以你还必须使用我调用TypeAdapter的另一个CustomReflectiveTypeAdapterFactory并且几乎是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

最重要的部分是read方法,我捕获异常并添加字段名称并再次抛出异常。请注意,类CustomTypeAdapterRuntimeTypeWrapper只是库内部的TypeAdapterRuntimeTypeWrapper的重命名副本,因为类是私有的。

因此,主要方法改变如下:

 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();

现在你有了这个堆栈跟踪(对异常的更改非常简单,我省略了它们):

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)

当然这个解决方案需要付出一些代价。

  • 首先,您必须复制一些私人/最终课程并进行更改。如果库得到更新,你必须再次检查你的代码(源代码的分支是相同的,但至少你不必复制所有代码)。
  • 如果您自定义字段排除策略,构造函数或字段命名策略,则必须将它们复制到CustomReflectiveTypeAdapterFactory中,因为我没有发现从构建器传递它们的可能性。