Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gson deserialize base class for a generic type adapter

Tags:

java

json

gson

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.

like image 667
driima Avatar asked Jan 18 '26 17:01

driima


1 Answers

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.

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

like image 136
Lyubomyr Shaydariv Avatar answered Jan 21 '26 05:01

Lyubomyr Shaydariv