代码之家  ›  专栏  ›  技术社区  ›  geschema

反序列化期间Gson检查枚举值

  •  3
  • geschema  · 技术社区  · 8 年前

    假设Java类中有以下枚举:

    enum AccessMode {
        READ_WRITE,
        READ_ONLY,
        WRITE_ONLY
    };
    

    只要JSON包含枚举字段的有效值,JSON反序列化就可以在Gson中正常工作,例如:

    "access": "READ_WRITE"
    

    不幸的是,fromJson()似乎在JSON中检测到无效的枚举值,例如:

    "access": "READ_XXX"
    

    在使用Gson反序列化JSON文件时,如何添加枚举值检查?

    2 回复  |  直到 8 年前
        1
  •  4
  •   Lyubomyr Shaydariv    8 年前

    从版本2.8.2开始,Gson不支持此类用例。 我认为这是值得作为一项特别建议提交给Gson开发团队的 GsonBuilder 配置方法。 现在最多可以编写一个几乎重复的自定义枚举类型适配器 com.google.gson.internal.bind.EnumTypeAdapter 功能,但添加名称检查。

    final class StrictEnumTypeAdapterFactory
            implements TypeAdapterFactory {
    
        private static final TypeAdapterFactory allStrictEnumTypeAdapterFactory = new StrictEnumTypeAdapterFactory(enumClass -> true);
    
        private final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass;
    
        private StrictEnumTypeAdapterFactory(final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass) {
            this.isStrictEnumClass = isStrictEnumClass;
        }
    
        static TypeAdapterFactory get(final Predicate<? super Class<? extends Enum<?>>> isStrictEnumClass) {
            return new StrictEnumTypeAdapterFactory(isStrictEnumClass);
        }
    
        static TypeAdapterFactory get() {
            return allStrictEnumTypeAdapterFactory;
        }
    
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            final Class<? super T> rawType = typeToken.getRawType();
            // Skip non-enums
            if ( !Enum.class.isAssignableFrom(rawType) ) {
                return null;
            }
            // Check if the enum is supported by the "strict" policy
            @SuppressWarnings("unchecked")
            final Class<? extends Enum<?>> enumRawType = (Class<? extends Enum<?>>) rawType;
            if ( !isStrictEnumClass.test(enumRawType) ) {
                return null;
            }
            // Trivial rawtypes/unchecked casts
            @SuppressWarnings({ "rawtypes", "unchecked" })
            final TypeAdapter<? extends Enum<?>> strictEnumTypeAdapter = StrictEnumTypeAdapter.get((Class) enumRawType);
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) strictEnumTypeAdapter;
            return castTypeAdapter;
        }
    
        private static final class StrictEnumTypeAdapter<E extends Enum<E>>
                extends TypeAdapter<E> {
    
            private final Class<E> enumClass;
            private final Map<String, E> nameToEnumConstant;
            private final Map<E, String> enumConstantToName;
    
            private StrictEnumTypeAdapter(final Class<E> enumClass, final Map<String, E> nameToEnumConstant, final Map<E, String> enumConstantToName) {
                this.enumClass = enumClass;
                this.nameToEnumConstant = nameToEnumConstant;
                this.enumConstantToName = enumConstantToName;
            }
    
            private static <E extends Enum<E>> TypeAdapter<E> get(final Class<E> enumClass) {
                final Map<String, E> nameToEnumConstant = new HashMap<>();
                final Map<E, String> enumConstantToName = new HashMap<>();
                final Map<String, E> enumNameToEnumConstant = Stream.of(enumClass.getEnumConstants())
                        .collect(Collectors.toMap(Enum::name, Function.identity()));
                Stream.of(enumClass.getFields())
                        // It can be either a simple enum constant, or an enum constant that overrides
                        .filter(field -> enumClass.isAssignableFrom(field.getType()))
                        .forEach(field -> {
                            final E enumConstant = enumNameToEnumConstant.get(field.getName());
                            // For compatibility with the original type adapter, we have to respect the @SeriaizedName annotation
                            final SerializedName serializedName = field.getAnnotation(SerializedName.class);
                            if ( serializedName == null ) {
                                nameToEnumConstant.put(field.getName(), enumConstant);
                                enumConstantToName.put(enumConstant, field.getName());
                            } else {
                                nameToEnumConstant.put(serializedName.value(), enumConstant);
                                enumConstantToName.put(enumConstant, serializedName.value());
                                for ( final String alternate : serializedName.alternate() ) {
                                    nameToEnumConstant.put(alternate, enumConstant);
                                }
                            }
                        });
                return new StrictEnumTypeAdapter<>(enumClass, nameToEnumConstant, enumConstantToName)
                        .nullSafe(); // A convenient method to handle nulls
            }
    
            @Override
            public void write(final JsonWriter out, final E value)
                    throws IOException {
                out.value(enumConstantToName.get(value));
            }
    
            @Override
            public E read(final JsonReader in)
                    throws IOException {
                final String key = in.nextString();
                // This is what the original type adapter probably misses
                if ( !nameToEnumConstant.containsKey(key) ) {
                    throw new JsonParseException(enumClass + " does not have an enum named " + key + " at " + in);
                }
                return nameToEnumConstant.get(key);
            }
    
        }
    
    }
    

    简单测试:

    private static final Gson gson = new GsonBuilder()
            .registerTypeAdapterFactory(StrictEnumTypeAdapterFactory.get())
            .create();
    
    public static void main(final String... args)
            throws IOException {
        try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q49572505.class, "good.json") ) {
            System.out.println(gson.<Status>fromJson(jsonReader, Status.class).access);
        }
        try ( final JsonReader jsonReader = Resources.getPackageResourceJsonReader(Q49572505.class, "bad.json") ) {
            try {
                gson.<Status>fromJson(jsonReader, Status.class);
                throw new AssertionError();
            } catch ( final JsonParseException ex ) {
                System.out.println(ex.getMessage());
            }
        }
    }
    

    输出:

    读\写
    q49572505级。AccessMode在第2行第22列路径$的JsonReader中没有名为READ\u XXX的枚举。通道

        2
  •  4
  •   iagreen    8 年前

    你可以看看@ Moshi . 我发现它是GSON的一个合适而直接的替代品,并且它已经支持这种行为。

    @Lyubomyr\u Shaydarlv的解决方案是可行的,但如果您不想复制GSON的内部代码,可以在自定义 TypeAdapterFactory . 运行适配器,如果它返回 null ,您知道该值无效。它的优点是继承并更改为默认值 enum 转换器。

    class StrictEnumTypeAdapterFactory implements TypeAdapterFactory {
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            Class<T> rawType = (Class<T>) type.getRawType();
            if (!rawType.isEnum()) {
                return null;
            }
            return newStrictEnumAdapter(gson.getDelegateAdapter(this, type));
        }
    
        private <T> TypeAdapter<T> newStrictEnumAdapter(
                final TypeAdapter<T> delegateAdapter) {
            return new TypeAdapter<T>() {
    
                @Override
                public void write(JsonWriter out, T value) throws IOException {
                    delegateAdapter.write(out, value);
                }
    
                @Override
                public T read(JsonReader in) throws IOException {
                    // Peek at the next value and save it for the error message
                    // if you don't need the offending value's actual name
                    String enumValue = in.nextString();
                    JsonReader delegateReader = new JsonReader(new StringReader('"' + enumValue + '"'));
                    T value = delegateAdapter.read(delegateReader);
                    delegateReader.close();
                    if (value == null) throw new IllegalStateException("Invalid enum value - " + enumValue);
                    return value;
                }
            };
        }
    }