简体   繁体   中英

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:

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:

{
    "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:

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. Is there any way I can get around this?


Here is my code for the GsonBuilder I'm using:

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. There's an article here which aided me with implementing that feature.

I don't think that JsonDeserializer is a good choice here:

  • You need either bind each type to the Gson instance in GsonBuilder that's a kind of error-prone, or use 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):

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.

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).

I think that one problem in your adapter is that it actually is never called for ExtendedKit only for Kit . So that is why it works with ExtendedKit , I guess. Gson can not handle generics by default anyway because of type erasure.

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...

I suggest that you declare a wrapper class for Kit like:

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

This is easier to deserialize with TypeAdapter like:

@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 = kitWrapper.getKit();

As Lyubomyr Shaydariv in his answer mentioned your case is almost suitable for 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. In other words, if you can change your Json & Kit accordingly to:

{
    "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? (even not Maven or Gradle user).

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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