【问题标题】:Gson deserialize base class for a generic type adapterGson 反序列化泛型类型适配器的基类
【发布时间】:2018-02-28 16:23:28
【问题描述】:

我有以下课程:

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

我有一个类扩展了 Kit 的附加功能:

public class ExtendedKit extends Kit {
    private String extraProperty;
}

使用 Gson,我希望能够反序列化这两个类以及更多不同类型,而无需为它们创建一堆类型适配器,因为它们都具有相同的 Json 结构:

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

它被传递到注册到我的 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);
        }
    }
}

事情是,这适用于ExtendedKit,但如果我只想反序列化一个Kit,没有extraProperty,它就不起作用,因为它在尝试调用context.deserialize() 时会导致NullPointerException在属性对象上。有什么办法可以解决这个问题吗?


这是我正在使用的 GsonBuilder 的代码:

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

注意:添加了 PostProcessExecutor,以便我可以将后处理应用于我反序列化的任何可以进行后处理的对象。有一篇文章 here 帮助我实现了该功能。

【问题讨论】:

  • 这样做 但是如果我想反序列化一个没有额外属性的 Kit 则它不起作用意味着你给 Json 没有额外的属性并且有 @987654330 @?
  • 是的。这是对的。很抱歉没有在我的问题中澄清这一点。我正在使用那种类型。

标签: java json gson


【解决方案1】:

我认为JsonDeserializer在这里不是一个好的选择:

  • 您需要将每个类型绑定到GsonBuilder 中的Gson 实例,这很容易出错,或者使用registerTypeHierarchyAdapter
  • 对于后者,您会遇到无限递归(如果我没记错的话:因为上下文只提供了一种反序列化 same 类型实例的机制)。

以下类型适配器工厂可以克服上述限制:

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

    }

}

仅针对您的Kit 类的使用示例(下面的方法参考仅检查Kit 是给定实际原始类的超类还是后者是Kit 本身):

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

请注意,您的问题不是唯一的,RuntimeTypeAdapterFactory 几乎涵盖了您的情况,但 RuntimeTypeAdapterFactory 不会像您的示例那样隔离 typeproperties

P.S. 请注意,这种类型适配器工厂远非真正的通用:它不适用于 types(类是类型的一种特殊情况),generic 类型等。如果感兴趣,但当然不是过度设计,您可能想参考我的解决方案,使用 Type 实例 object serialization mechanism绑定到特定平台)或使用类型和泛型类型表示法parsing using JParsec(两个链接都指向俄语 StackExchange 站点)。

【讨论】:

  • 这很棒。这正是我想要的,我仍然可以使用我的 PostProcessExecutor。不过,我正在尝试解决一个小问题。我有两种后期处理——第一种只是使用空的 postProcess() 方法进行后期处理。如果我不需要任何其他信息,这很好,但在某些情况下,我的 json 中有一些我不想临时存储并想评估的数据,所以我有另一个 postProcess 方法传递“属性” " 来自 json 的对象。有没有办法获得相同的功能? postProcess(JsonObject properties);
  • 没关系 - 我刚刚创建了一个额外的类型适配器工厂来处理这个问题。再次感谢您的帮助!
  • 我在将生成的 json 反序列化为实际对象时发现了一些问题。
【解决方案2】:

我认为您的适配器中的一个问题是它实际上从未为ExtendedKit 只为Kit 调用。所以这就是为什么它工作ExtendedKit,我猜。由于类型擦除,Gson 默认无法处理泛型。

尽管如此:声明要反序列化的对象就像 Json 呈现的那样是一种很好且明确的做法,因为它通常会减少适配器等逻辑的编码...

我建议你为Kit 声明一个包装类,例如:

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

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

所以注册这个适配器并通过以下方式获取 Kit:

Kit kit = kitWrapper.getKit();

正如his answer 中的Lyubomyr Shaydariv 所说,您的情况几乎适合RunTimeAdapterFactory。似乎type 应该是反序列化对象的属性,而不是 top-level 属性和较低级别的实际对象。换句话说,如果您可以将您的 Json & Kit 相应地更改为:

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

它可能工作正常。在这种情况下,您可能会对How to add Gson extras to an Android project? 感兴趣(即使不是 Maven 或 Gradle 用户)。

但如果不能,那么我建议使用包装类。

但是,正如您所见,自己编写代码也没什么大不了的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-08-26
    • 1970-01-01
    • 1970-01-01
    • 2015-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多