java 在反序列化过程中检查枚举值

yhqotfr8  于 2023-06-20  发布在  Java
关注(0)|答案(3)|浏览(94)

假设Java类中有以下枚举:

enum AccessMode {
    READ_WRITE,
    READ_ONLY,
    WRITE_ONLY
};

JSON反序列化与Gson一起工作得很好,只要JSON包含枚举字段的有效值,例如:

"access": "READ_WRITE"

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

"access": "READ_XXX"

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

gk7wooem

gk7wooem1#

从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在JsonReader第2行第22列路径$.access中没有名为READ_XXX的枚举

wqsoz72f

wqsoz72f2#

您可以查看@Moshi。我发现它是GSON的一个合适且直接的替代品,并且它已经支持这种行为。
@Lyubomyr_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;
            }
        };
    }
}
ukqbszuj

ukqbszuj3#

对于交叉引用,我想将未知值重新Map到enum类型中的 fallback value。如果在另一端字段有新值,但代码尚未对其进行反序列化,这将非常有用。
https://github.com/google/gson/issues/608#issuecomment-1592929122
我为gson编写了这个通用的TypeAdapterFactory
在Gson注册:

GsonBuilder().registerTypeAdapterFactory(EnumWithFallbackValueTypeAdapterFactory()).create()

注解要重新Map到的枚举值

enum class Kind {
    @FallbackValue
    UNKNOWN,
    K1,
    K2
}

现在像这样的有效载荷将具有重新Map到UNKNOWN的kind值。

{ "kind" : "K3" }
import com.google.gson.Gson
import com.google.gson.JsonPrimitive
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.io.IOException

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class UnknownValue

object EnumWithFallbackValueTypeAdapterFactory : TypeAdapterFactory {
    private val logger = thisLogger()

    override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
        if (!type.rawType.isEnum) {
            return null
        }

        val candidates = type.rawType.fields
            .asSequence()
            .filter { it.type == type.rawType && it.isAnnotationPresent(FallbackValue::class.java) }
            .map { field ->
                @Suppress("UNCHECKED_CAST")
                type.rawType.enumConstants.single { enumValue ->
                    field.get(null) == enumValue
                } as T
            }
            .toList()

        val delegate = gson.getDelegateAdapter(this, type)

        return when {
            candidates.isEmpty() -> delegate
            candidates.size > 1 -> throw IllegalArgumentException("Only one enum value can be annotated with @${FallbackValue::class.java.simpleName}")
            else -> {
                val fallbackValue = candidates.single()
                object : TypeAdapter<T>() {
                    @Throws(IOException::class)
                    override fun write(writer: JsonWriter, value: T) {
                        delegate.write(writer, value)
                    }

                    @Throws(IOException::class)
                    override fun read(reader: JsonReader): T {
                        // Keep null as undefined
                        if (reader.peek() == JsonToken.NULL) {
                            reader.nextNull() // consume
                            @Suppress("UNCHECKED_CAST")
                            return null as T
                        }
                        val rawString = reader.nextString()
                        val fromDelegate = delegate.fromJsonTree(JsonPrimitive(rawString))
                        return if (fromDelegate != null) {
                            fromDelegate
                        } else {
                            logger.debug { "Unknown enum value: $rawString for ${type.rawType.name}" }
                            fallbackValue
                        }
                    }
                }
            }
        }
    }
}

相关问题