Search code examples
javajsongsonmultimaptype-erasure

Gson adapter for a heterogeneous Multimap


I have a Multimap<Class<?>, Object> populated like

multimap.put(o.getClass, o)

i.e., each object gets put in a proper bucket according to its class. I need to serialize and deserialize the multimap using Gson. All the objects belong to simple classes having no type parameters. I mean, each of them can be deserialized by using gson.fromJson(json, someClass); no TypeToken needed here.

If it helps, I could use a TypeToken or whatever as the key; I don't care. All used classes subclass a class of mine, if it helps. What I don't want is splitting the multimap into multiple homogeneous lists as there will be tens of them. As it's actually an ImmutableMultimap, it'd mean many more lines which I want to avoid.

What I've tried: Not worth mentioning. None of the Adapters I wrote or saw does anything similar.


Solution

  • If I understand you correctly, you can accomplish such a type adapter relatively easy.

    First, let's create a Multimap type adapter. The following Multimap type adapter can work with any multimap, however Class-related keys will be specialized below.

    final class MultimapTypeAdapter<K, V>
            extends TypeAdapter<Multimap<K, V>> {
    
        private final Converter<K, String> keyConverter;
        private final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider;
    
        private MultimapTypeAdapter(
                final Converter<K, String> keyConverter,
                final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
        ) {
            this.keyConverter = keyConverter;
            this.valueTypeAdapterProvider = valueTypeAdapterProvider;
        }
    
        static <K, V> TypeAdapter<Multimap<K, V>> multimapTypeAdapter(
                final Converter<K, String> keyConverter,
                final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
        ) {
            return new MultimapTypeAdapter<>(keyConverter, valueTypeAdapterProvider).nullSafe();
        }
    
        @Override
        @SuppressWarnings("resource")
        public void write(final JsonWriter jsonWriter, final Multimap<K, V> multimap)
                throws IOException {
            jsonWriter.beginObject();
            for ( final K key : multimap.keySet() ) {
                jsonWriter.name(keyConverter.convert(key));
                final TypeAdapter<? super V> typeAdapter = valueTypeAdapterProvider.apply(key);
                jsonWriter.beginArray();
                for ( final V value : multimap.get(key) ) {
                    typeAdapter.write(jsonWriter, value);
                }
                jsonWriter.endArray();
            }
            jsonWriter.endObject();
        }
    
        @Override
        public Multimap<K, V> read(final JsonReader jsonReader)
                throws IOException {
            final ImmutableMultimap.Builder<K, V> multimapBuilder = new ImmutableMultimap.Builder<>();
            jsonReader.beginObject();
            while ( jsonReader.hasNext() ) {
                final K key = keyConverter.reverse().convert(jsonReader.nextName());
                final TypeAdapter<V> typeAdapter = valueTypeAdapterProvider.apply(key);
                jsonReader.beginArray();
                while ( jsonReader.hasNext() ) {
                    final V value = typeAdapter.read(jsonReader);
                    multimapBuilder.put(key, value);
                }
                jsonReader.endArray();
            }
            jsonReader.endObject();
            return multimapBuilder.build();
        }
    
    }
    

    Now, you can create a simple Class key converter: The converter is pretty straight-forward and self-descriptive. More complex converting strategies can be found, for example, here and here (the latter does not support arrays in full).

    final class ClassKeyConverter
            extends Converter<Class<?>, String> {
    
        private static final Converter<Class<?>, String> classKeyConverter = new ClassKeyConverter();
    
        private ClassKeyConverter() {
        }
    
        static Converter<Class<?>, String> classKeyConverter() {
            return classKeyConverter;
        }
    
        @Override
        protected String doForward(final Class<?> a) {
            return a.toString();
        }
    
        @Override
        public Class<?> doBackward(final String b) {
            final Class<?> primitiveType = primitiveTypes.get(b);
            if ( primitiveType != null ) {
                return primitiveType;
            }
            final int prefix = b.startsWith(CLASS) ? CLASS.length()
                    : b.startsWith(INTERFACE) ? INTERFACE.length()
                    : -1;
            if ( prefix >= 0 ) {
                try {
                    return Class.forName(b.substring(prefix));
                } catch ( final ClassNotFoundException ex ) {
                    throw new RuntimeException(ex);
                }
            }
            throw new IllegalArgumentException(b);
        }
    
        private static final Map<String, Class<?>> primitiveTypes = ImmutableMap.<String, Class<?>>builder()
                .put("boolean", boolean.class)
                .put("byte", byte.class)
                .put("short", short.class)
                .put("int", int.class)
                .put("long", long.class)
                .put("float", float.class)
                .put("double", double.class)
                .put("char", char.class)
                .build();
    
        private static final String CLASS = "class ";
        private static final String INTERFACE = "interface ";
    
    }
    

    And now you can create a type adapter factory that can process such a multimap:

    final class ClassKeyMultimapTypeAdapterFactory
            implements TypeAdapterFactory {
    
        private static final TypeAdapterFactory classKeyMultimapTypeAdapterFactory = new ClassKeyMultimapTypeAdapterFactory();
    
        static final Type classKeyMultimapType = TypeToken.getParameterized(Multimap.class, Class.class, Object.class).getType();
    
        private ClassKeyMultimapTypeAdapterFactory() {
        }
    
        static TypeAdapterFactory classKeyMultimapTypeAdapterFactory() {
            return classKeyMultimapTypeAdapterFactory;
        }
    
        @Override
        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
            if ( !isClassKeyMultimap(typeToken) ) {
                return null;
            }
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) multimapTypeAdapter(classKeyConverter(), type -> gson.getDelegateAdapter(this, TypeToken.get(type)));
            return typeAdapter;
        }
    
        private static boolean isClassKeyMultimap(final TypeToken<?> typeToken) {
            if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
                final Type type = typeToken.getType();
                if ( type instanceof ParameterizedType ) {
                    final ParameterizedType parameterizedType = (ParameterizedType) type;
                    if ( Class.class.equals(parameterizedType.getActualTypeArguments()[0]) ) {
                        // We expect to process `Multimap<Class<?>, ?>` only
                        return true;
                    }
                }
            }
            return false;
        }
    
    }
    

    Finally, you can test it out:

    private static final Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .registerTypeAdapterFactory(classKeyMultimapTypeAdapterFactory())
            .create();
    
    public static void main(final String... args) {
        final Multimap<Class<?>, Object> multimapBefore = ImmutableMultimap.<Class<?>, Object>builder()
                .put(int.class, 2)
                .put(int.class, 3)
                .put(int.class, 4)
                .put(Integer.class, 2)
                .put(Integer.class, 3)
                .put(Integer.class, 4)
                .put(String.class, "foo")
                .put(String.class, "bar")
                .put(String.class, "baz")
                .build();
        System.out.println(multimapBefore);
        final String json = gson.toJson(multimapBefore, classKeyMultimapType);
        System.out.println(json);
        final Multimap<Class<?>, Object> multimapAfter = gson.fromJson(json, classKeyMultimapType);
        System.out.println(multimapAfter);
        if ( !multimapBefore.equals(multimapAfter) ) {
            throw new AssertionError("multimaps do not equal");
        }
    }
    

    Output:

    {int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}
    {"int":[2,3,4],"class java.lang.Integer":[2,3,4],"class java.lang.String":["foo","bar","baz"]}
    {int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}
    

    Update 1

    Ok, let's proceed making the isClassKeyMultimap method a bit smarter.

    I'd like to work with a Multimap<Class<Something>, Something>, too.

    You're, I guess, are talking about TypeToken literals. Yep, I used TypeToken.getParameterized(...) forgetting that a Class instance can be parameterized as well. All you have to do to make it smarter is just adding an additional check to the method.

    if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
        final Type type = typeToken.getType();
        if ( type instanceof ParameterizedType ) {
            final ParameterizedType parameterizedType = (ParameterizedType) type;
            final Type actualTypeArg0 = parameterizedType.getActualTypeArguments()[0];
            // raw java.lang.Class (Class.class, Class.forName("java.lang.Class"), etc)
            if ( actualTypeArg0 == Class.class ) {
                return true;
            }
            // or maybe it's something like a Class<...> instance that:
            // * can be generated by javac when you parameterize a type (this is why Gson TypeToken's look "weird")
            // * or create a ParameterizedType instance yourself, say using TypeToken.getParameterized or your custom ParameterizedType implementation
            if ( actualTypeArg0 instanceof ParameterizedType && ((ParameterizedType) actualTypeArg0).getRawType() == Class.class ) {
                return true;
            }
        }
    }
    return false;
    

    The ad-hoc comments should explain why the previous implementation did not cover all the cases. Even more, you can write a reusable utility method that would recognize the raw class itself.

    private static Type getRawClass(final Type type) {
        if ( type instanceof ParameterizedType ) {
            return ((ParameterizedType) type).getRawType();
        }
        return type;
    }
    

    And then those two checks could be collapsed into a single one:

    if ( getRawClass(actualTypeArg0) == Class.class ) {
        return true;
    }
    

    == should work just fine with java.class.Class instances since its instances are effective flyweights, and can improve readability here.