简体   繁体   English

Gson 反序列化泛型类型适配器的基类

[英]Gson deserialize base class for a generic type adapter

I have the following class:我有以下课程:

public class Kit {
    private String name;
    private int num;
}

And I have a class which extends Kit with additional functionality:我有一个类,它扩展了 Kit 的附加功能:

public class ExtendedKit extends Kit {
    private String extraProperty;
}

With Gson, I want to be able to deserialize both of these classes, and more of different types, without creating a bunch of type adapters for them, as they all have the same Json structure:使用 Gson,我希望能够反序列化这两个类以及更多不同类型,而无需为它们创建一堆类型适配器,因为它们都具有相同的 Json 结构:

{
    "type": "com.driima.test.ExtendedKit",
    "properties": {
        "name": "An Extended Kit",
        "num": 124,
        "extra_property": "An extra property"
    }
}

Which is passed into the following type adapter registered to my GsonBuilder:它被传递到注册到我的 GsonBuilder 的以下类型适配器:

public class GenericAdapter<T> implements JsonDeserializer<T> {
    @Override
    public T deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
        final JsonObject object = json.getAsJsonObject();
        String classType = object.get("type").getAsString();
        JsonElement element = object.get("properties");

        try {
            return context.deserialize(element, Class.forName(classType));
        } catch (ClassNotFoundException e) {
            throw new JsonParseException("Unknown element type: " + type, e);
        }
    }
}

Thing is, that works for ExtendedKit , but it doesn't work if I want to deserialize just a Kit , without the extraProperty, as it calls causes a NullPointerException when it tries to call context.deserialize() on the properties object.事实是,这适用于ExtendedKit ,但如果我只想反序列化一个Kit ,而没有 extraProperty ,则它不起作用,因为当它尝试在属性对象上调用 context.deserialize() 时,它调用会导致 NullPointerException 。 Is there any way I can get around this?有什么办法可以解决这个问题吗?


Here is my code for the GsonBuilder I'm using:这是我正在使用的 GsonBuilder 的代码:

private static final GsonBuilder GSON_BUILDER = new GsonBuilder()
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        .registerTypeAdapterFactory(new PostProcessExecutor())
        .registerTypeAdapter(Kit.class, new GenericAdapter<Kit>());

Note: PostProcessExecutor is added so that I can apply post-processing to any object I deserialize that can be post-processed.注意:添加了 PostProcessExecutor 以便我可以对我反序列化的任何可以进行后处理的对象应用后处理。 There's an article here which aided me with implementing that feature.有一篇文章在这里它帮助我实现这个功能。

I don't think that JsonDeserializer is a good choice here:我认为JsonDeserializer在这里不是一个好的选择:

  • You need either bind each type to the Gson instance in GsonBuilder that's a kind of error-prone, or use registerTypeHierarchyAdapter .您需要将每个类型绑定到GsonBuilder中的Gson实例,这很容易出错,或者使用registerTypeHierarchyAdapter
  • For the latter you'd run into infinite recursion (if I'm not wrong: because the context only provides a mechanism to deserialize an instance of the same type).对于后者,您会遇到无限递归(如果我没记错的话:因为上下文仅提供了一种反序列化相同类型实例的机制)。

The following type adapter factory can overcome the above limitations:以下类型的适配器工厂可以克服上述限制:

final class PolymorphicTypeAdapterFactory
        implements TypeAdapterFactory {

    // Let's not hard-code `Kit.class` here and let a user pick up types at a call-site
    private final Predicate<? super Class<?>> predicate;

    private PolymorphicTypeAdapterFactory(final Predicate<? super Class<?>> predicate) {
        this.predicate = predicate;
    }

    static TypeAdapterFactory get(final Predicate<? super Class<?>> predicate) {
        return new PolymorphicTypeAdapterFactory(predicate);
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        final Class<? super T> rawClass = typeToken.getRawType();
        if ( !predicate.test(rawClass) ) {
            // Something we cannot handle? Try pick the next best type adapter factory
            return null;
        }
        // This is what JsonDeserializer fails at:
        final TypeAdapter<T> writeTypeAdapter = gson.getDelegateAdapter(this, typeToken);
        // Despite it's possible to use the above type adapter for both read and write, what if the `type` property points to another class?
        final Function<? super Class<T>, ? extends TypeAdapter<T>> readTypeAdapterResolver = actualRawClass -> {
            if ( !rawClass.isAssignableFrom(actualRawClass) ) {
                throw new IllegalStateException("Cannot parse as " + actualRawClass);
            }
            return gson.getDelegateAdapter(this, TypeToken.get(actualRawClass));
        };
        return PolymorphicTypeAdapter.get(rawClass, writeTypeAdapter, readTypeAdapterResolver);
    }

    private static final class PolymorphicTypeAdapter<T>
            extends TypeAdapter<T> {

        private final Class<? super T> rawClass;
        private final TypeAdapter<T> writeTypeAdapter;
        private final Function<? super Class<T>, ? extends TypeAdapter<T>> readTypeAdapterResolver;

        private PolymorphicTypeAdapter(final Class<? super T> rawClass, final TypeAdapter<T> writeTypeAdapter,
                final Function<? super Class<T>, ? extends TypeAdapter<T>> readTypeAdapterResolver) {
            this.rawClass = rawClass;
            this.writeTypeAdapter = writeTypeAdapter;
            this.readTypeAdapterResolver = readTypeAdapterResolver;
        }

        // Since constructors are meant only to assign parameters to fields, encapsulate the null-safety handling in the factory method
        private static <T> TypeAdapter<T> get(final Class<? super T> rawClass, final TypeAdapter<T> writeTypeAdapter,
                final Function<? super Class<T>, ? extends TypeAdapter<T>> readTypeAdapterResolver) {
            return new PolymorphicTypeAdapter<>(rawClass, writeTypeAdapter, readTypeAdapterResolver)
                    .nullSafe();
        }

        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter jsonWriter, final T value)
                throws IOException {
            jsonWriter.beginObject();
            jsonWriter.name("type");
            jsonWriter.value(rawClass.getName());
            jsonWriter.name("properties");
            writeTypeAdapter.write(jsonWriter, value);
            jsonWriter.endObject();
        }

        @Override
        public T read(final JsonReader jsonReader)
                throws IOException {
            jsonReader.beginObject();
            // For simplicity's sake, let's assume that the class property `type` always precedes the `properties` property
            final Class<? super T> actualRawClass = readActualRawClass(jsonReader);
            final T value = readValue(jsonReader, actualRawClass);
            jsonReader.endObject();
            return value;
        }

        private Class<? super T> readActualRawClass(final JsonReader jsonReader)
                throws IOException {
            try {
                requireProperty(jsonReader, "type");
                final String value = jsonReader.nextString();
                @SuppressWarnings("unchecked")
                final Class<? super T> actualRawClass = (Class<? super T>) Class.forName(value);
                return actualRawClass;
            } catch ( final ClassNotFoundException ex ) {
                throw new AssertionError(ex);
            }
        }

        private T readValue(final JsonReader jsonReader, final Class<? super T> rawClass)
                throws IOException {
            requireProperty(jsonReader, "properties");
            @SuppressWarnings("unchecked")
            final Class<T> castRawClass = (Class<T>) rawClass;
            final TypeAdapter<T> readTypeAdapter = readTypeAdapterResolver.apply(castRawClass);
            return readTypeAdapter.read(jsonReader);
        }

        private static void requireProperty(final JsonReader jsonReader, final String propertyName)
                throws IOException {
            final String name = jsonReader.nextName();
            if ( !name.equals(propertyName) ) {
                throw new JsonParseException("Unexpected property: " + name);
            }
        }

    }

}

An example of use that's specific for your Kit class only (the method reference below just checks if Kit is a super-class of the given actual raw class or the latter is Kit itself):一个特定于您的Kit类的使用示例(下面的方法参考只是检查Kit是否是给定实际原始类的超类,或者后者是Kit本身):

private static final Gson gson = new GsonBuilder()
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        .registerTypeAdapterFactory(PolymorphicTypeAdapterFactory.get(Kit.class::isAssignableFrom))
        .create();

Note that your problem is not unique, and your case is almost covered with RuntimeTypeAdapterFactory , but RuntimeTypeAdapterFactory does not segregate type and properties as your example does.请注意,您的问题不是唯一的,您的情况几乎涵盖了RuntimeTypeAdapterFactory ,但RuntimeTypeAdapterFactory并没有像您的示例那样分离typeproperties

PS Note that this type adapter factory is far way from being truly generic: it does not work with types (classes are a particular case of types), generic types, etc. In case of interest, but not over-engineering of course, you might like to reference my solution for encoding types with their parameterization either using Type instances object serialization mechanism (too cryptic and tightly bound to a specific platform) or using types and generic types notation parsing using JParsec (both links refer the Russian-speaking StackExchange site). PS请注意,这种类型适配器工厂远非真正的泛型:它不适用于类型(类是类型的特殊情况)、泛型类型等。可能想参考我的解决方案,使用Type实例对象序列化机制(过于神秘且与特定平台紧密绑定)或使用 JParsec 解析类型和泛型类型符号(两个链接均指俄语StackExchange站点) )。

I think that one problem in your adapter is that it actually is never called for ExtendedKit only for Kit .我认为您的适配器中的一个问题是它实际上从来没有只为Kit调用ExtendedKit So that is why it works with ExtendedKit , I guess.所以这就是为什么它适用ExtendedKit ,我猜。 Gson can not handle generics by default anyway because of type erasure.由于类型擦除,Gson 在默认情况下无法处理泛型。

Be that as it may: it is a good and clear practice to declare the object that is to be deserialized just as the Json presents since it usually reduces the coding of the logic for adapters etc...尽管如此:正如 Json 所呈现的那样,声明要反序列化的对象是一种很好且清晰的做法,因为它通常会减少适配器等的逻辑编码......

I suggest that you declare a wrapper class for Kit like:我建议您为Kit声明一个包装类,例如:

@Getter
@AllArgsConstructor
public class KitWrapper {
    private String type;
    @SerializedName("properties") // 
    private Kit kit;
}

This is easier to deserialize with TypeAdapter like:这更容易用TypeAdapter反序列化,例如:

@Slf4j
public class KitWrapperAdapter implements JsonDeserializer<KitWrapper>  {
    @Override
    public KitWrapper deserialize(JsonElement json, Type typeOfT,
                   JsonDeserializationContext context)
            throws JsonParseException {
        try {
            @SuppressWarnings("unchecked")
            Class<? extends Kit> classKit =
                    (Class<? extends Kit>)Class.forName(json.getAsJsonObject()
                         .get("type").getAsString() );
            JsonElement jeProperties = json.getAsJsonObject().get("properties");
            Kit kit = context.deserialize(jeProperties, classKit);
            // Not needed to parse anymore, new KitWrapper can be created
            // with this information
            return new KitWrapper(classKit.getName(), kit);

        } catch (Exception e) {
            log.error("{}", e.toString());
            return null;
        }
    }
}

So register this adapter and get Kit by:因此,注册此适配器并通过以下方式获取 Kit:

Kit kit = kitWrapper.getKit();

As Lyubomyr Shaydariv in his answer mentioned your case is almost suitable for RunTimeAdapterFactory .正如Lyubomyr Shaydariv他的回答中提到的,你的情况几乎适用RunTimeAdapterFactory It just seems that the type should be a property of the deserialized object, not so that it is top-level property and the actual object as lower level.看起来type应该是反序列化对象的属性,而不是它是顶级属性,而实际对象是较低级别。 In other words, if you can change your Json & Kit accordingly to:换句话说,如果您可以相应地将 Json & Kit更改为:

{
    "type": "com.driima.test.ExtendedKit",
    "name": "An Extended Kit",
    "num": 124,
    "extra_property": "An extra property"
}

it might work fine.它可能工作正常。 In that case you might be interested of How to add Gson extras to an Android project?在这种情况下,您可能对如何向 Android 项目添加 Gson extras感兴趣 (even not Maven or Gradle user). (即使不是 Maven 或 Gradle 用户)。

But if can not then i suggest that wrapper class.但如果不能,那么我建议使用包装类。

However, as you can see it is not so big deal to code by yourself either.但是,正如您所看到的,自己编写代码也没什么大不了的。

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

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