How to customize MapTypeAdapterFactory

674 views
Skip to first unread message

Carlos Belizón Ibañez

unread,
Oct 20, 2016, 10:09:33 AM10/20/16
to google-gson
I am on the necessity of customize MapTypeAdapterFactory to serialize and deserialize Maps in this way:

"features": {
    "entry": [
      {
        "key": "key1",
        "value": "value1"
      },
      {
        "key": "key2",
        "value": "value2"
      },
      {
        "key": "key3,
        "value": "value3"
      }
    ]
  }



But I stuck on only trying to make Gson to work with a custom MapTypeAdapterFactory. This is what I tried:

import com.google.gson.*
import com.google.gson.internal.*
import com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper
import com.google.gson.internal.bind.TypeAdapters
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter

import java.awt.*
import java.lang.reflect.Type

class CustomMapTypeAdapterFactory implements TypeAdapterFactory {
private ConstructorConstructor constructorConstructor;
private boolean complexMapKeySerialization;

public CustomMapTypeAdapterFactory() {}

public CustomMapTypeAdapterFactory(ConstructorConstructor constructorConstructor, boolean complexMapKeySerialization) {
this.constructorConstructor = constructorConstructor;
this.complexMapKeySerialization = complexMapKeySerialization;
}

@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Type type = typeToken.getType();
Class rawType = typeToken.getRawType();
if (!Map.class.isAssignableFrom(rawType)) {
return null;
} else {
Class rawTypeOfSrc = $Gson$Types.getRawType(type);
Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc);
TypeAdapter keyAdapter = this.getKeyAdapter(gson, keyAndValueTypes[0]);
TypeAdapter valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1]));
ObjectConstructor constructor = this.constructorConstructor.get(typeToken);
CustomMapTypeAdapterFactory.Adapter result = new CustomMapTypeAdapterFactory.Adapter(gson, keyAndValueTypes[0], keyAdapter, keyAndValueTypes[1], valueAdapter, constructor);
return result;
}
}

private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) {
return keyType != Boolean.TYPE && keyType != Boolean.class ? context.getAdapter(TypeToken.get(keyType)) : TypeAdapters.BOOLEAN_AS_STRING;
}

private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> {
private final TypeAdapter<K> keyTypeAdapter;
private final TypeAdapter<V> valueTypeAdapter;
private final ObjectConstructor<? extends Map<K, V>> constructor;

public Adapter(Gson var1, Type context, TypeAdapter<K> keyType, Type keyTypeAdapter, TypeAdapter<V> valueType, ObjectConstructor<? extends Map<K, V>> valueTypeAdapter) {
this.keyTypeAdapter = new TypeAdapterRuntimeTypeWrapper(context, keyTypeAdapter, keyType);
this.valueTypeAdapter = new TypeAdapterRuntimeTypeWrapper(context, valueTypeAdapter, valueType);
this.constructor = constructor;
}

public Map<K, V> read(JsonReader input) throws IOException {
JsonToken peek =
input.peek();
if (peek == JsonToken.NULL) {
input.nextNull();
return null;
} else {
Map map = (Map) this.constructor.construct();
Object key;
Object value;
Object replaced;
if (peek == JsonToken.BEGIN_ARRAY) {
input.beginArray();

while (input.hasNext()) {
input.beginArray();
key = this.keyTypeAdapter.read(input);
value = this.valueTypeAdapter.read(input);
replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}

input.endArray();
}

input.endArray();
} else {
input.beginObject();

while (input.hasNext()) {
JsonReaderInternalAccess.INSTANCE.promoteNameToValue(input);
key = this.keyTypeAdapter.read(input);
value = this.valueTypeAdapter.read(input);
replaced = map.put(key, value);
if (replaced != null) {
throw new JsonSyntaxException("duplicate key: " + key);
}
}

input.endObject();
}

return map;
}
}

public void write(JsonWriter out, Map<K, V> map) throws IOException {
if (map == null) {
out.nullValue();
} else if (!CustomMapTypeAdapterFactory.this.complexMapKeySerialization) {
out.beginObject();
Iterator var9 = map.entrySet().iterator();

while (var9.hasNext()) {
Map.Entry var10 = (Map.Entry) var9.next();
out.name(String.valueOf(var10.getKey()));
this.valueTypeAdapter.write(out, var10.getValue());
}

out.endObject();
} else {
boolean hasComplexKeys = false;
ArrayList keys = new ArrayList(map.size());
ArrayList values = new ArrayList(map.size());

JsonElement keyElement1;
for (Iterator i = map.entrySet().iterator(); i.hasNext(); hasComplexKeys |= keyElement1.isJsonArray() || keyElement1.isJsonObject()) {
Map.Entry keyElement = (Map.Entry) i.next();
keyElement1 = this.keyTypeAdapter.toJsonTree(keyElement.getKey());
keys.add(keyElement1);
values.add(keyElement.getValue());
}

int var11;
if (hasComplexKeys) {
out.beginArray();

for (var11 = 0; var11 < keys.size(); ++var11) {
out.beginArray();
Streams.write((JsonElement) keys.get(var11), out);
this.valueTypeAdapter.write(out, values.get(var11));
out.endArray();
}

out.endArray();
} else {
out.beginObject();

for (var11 = 0; var11 < keys.size(); ++var11) {
JsonElement var12 = (JsonElement) keys.get(var11);
out.name(this.keyToString(var12));
this.valueTypeAdapter.write(out, values.get(var11));
}

out.endObject();
}

}
}

private String keyToString(JsonElement keyElement) {
if (keyElement.isJsonPrimitive()) {
JsonPrimitive primitive = keyElement.getAsJsonPrimitive();
if (primitive.isNumber()) {
return String.valueOf(primitive.getAsNumber());
} else if (primitive.isBoolean()) {
return Boolean.toString(primitive.getAsBoolean());
} else if (primitive.isString()) {
return primitive.getAsString();
} else {
throw new AssertionError();
}
} else if (keyElement.isJsonNull()) {
return "null";
} else {
throw new AssertionError();
}
}
}
}

ConstructorConstructor constructorConstructor = new ConstructorConstructor(new HashMap<Type, InstanceCreator<?>>());
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new CustomMapTypeAdapterFactory(constructorConstructor, false)).create();

Map<Point, String> original = new HashMap<>();
original.put("1", "a");
original.put("2", "b");
System.out.println(gson.toJson(original));

But I got the following error:

Caught: groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper(sun.reflect.generics.reflectiveObjects.TypeVariableImpl, sun.reflect.generics.reflectiveObjects.TypeVariableImpl, com.google.gson.internal.bind.ObjectTypeAdapter)
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper(sun.reflect.generics.reflectiveObjects.TypeVariableImpl, sun.reflect.generics.reflectiveObjects.TypeVariableImpl, com.google.gson.internal.bind.ObjectTypeAdapter)
  at CustomMapTypeAdapterFactory$Adapter.<init>(gsonTest.groovy:51)
       at CustomMapTypeAdapterFactory.create(gsonTest.groovy:36)
       at com.google.gson.Gson.getAdapter(Gson.java:360)
       at com.google.gson.Gson.toJson(Gson.java:597)
   at com.google.gson.Gson.toJson(Gson.java:584)
   at com.google.gson.Gson.toJson(Gson.java:539)
   at com.google.gson.Gson.toJson(Gson.java:519)
   at com.google.gson.Gson$toJson.call(Unknown Source)
     at gsonTest.run(gsonTest.groovy:184)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

Process finished with exit code 1



Is there any way to override the default processing of maps in Gson?

Thanks

Carlos Belizón Ibañez

unread,
Oct 20, 2016, 11:49:14 AM10/20/16
to google-gson
I managed myself to do it in this way. First create the Factory:

package com.tgifridays.ws.gson.typeadapterfactory;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.internal.Streams;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.apache.log4j.Logger;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author cbelizon
 */
public class CustomMapTypeAdapterFactory implements TypeAdapterFactory {
    private static final Logger LOG = Logger.getLogger(CustomMapTypeAdapterFactory.class);
    private final ConstructorConstructor constructorConstructor;
    final boolean complexMapKeySerialization;

    public CustomMapTypeAdapterFactory(ConstructorConstructor constructorConstructor, boolean complexMapKeySerialization) {
        this.constructorConstructor = constructorConstructor;
        this.complexMapKeySerialization = complexMapKeySerialization;
    }

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        Type type = typeToken.getType();

        Class<? super T> rawType = typeToken.getRawType();
        if (!Map.class.isAssignableFrom(rawType)) {
            return null;
        }

        Class<?> rawTypeOfSrc = $Gson$Types.getRawType(type);
        Type[] keyAndValueTypes = $Gson$Types.getMapKeyAndValueTypes(type, rawTypeOfSrc);
        TypeAdapter<?> keyAdapter = getKeyAdapter(gson, keyAndValueTypes[0]);
        TypeAdapter<?> valueAdapter = gson.getAdapter(TypeToken.get(keyAndValueTypes[1]));
        ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);

        // we don't define a type parameter for the key or value types
        TypeAdapter<T> result = new Adapter(gson, keyAndValueTypes[0], keyAdapter, keyAndValueTypes[1], valueAdapter, constructor);
        return result;
    }

    /**
     * Returns a type adapter that writes the value as a string.
     */
    private TypeAdapter<?> getKeyAdapter(Gson context, Type keyType) {
        return (keyType == boolean.class || keyType == Boolean.class) ? TypeAdapters.BOOLEAN_AS_STRING : context.getAdapter(TypeToken.get(keyType));
    }

    private final class Adapter<K, V> extends TypeAdapter<Map<K, V>> {
        private final TypeAdapter<K> keyTypeAdapter;
        private final TypeAdapter<V> valueTypeAdapter;
        private final ObjectConstructor<? extends Map<K, V>> constructor;

        public Adapter(Gson context, Type keyType, TypeAdapter<K> keyTypeAdapter, Type valueType, TypeAdapter<V> valueTypeAdapter, ObjectConstructor<? extends Map<K, V>> constructor) {
            this.keyTypeAdapter = new CustomTypeAdapterRuntimeTypeWrapper<K>(context, keyTypeAdapter, keyType);
            this.valueTypeAdapter = new CustomTypeAdapterRuntimeTypeWrapper<V>(context, valueTypeAdapter, valueType);
            this.constructor = constructor;
        }

        @Override
        public Map<K, V> read(JsonReader inner) throws IOException {
            JsonToken peek = inner.peek();
            if (peek == JsonToken.NULL) {
                inner.nextNull();
                return null;
            }

            Map<K, V> map = constructor.construct();

            if (peek == JsonToken.BEGIN_ARRAY) {
                inner.beginArray();
                while (inner.hasNext()) {
                    inner.beginArray(); // entry array
                    K key = keyTypeAdapter.read(inner);
                    V value = valueTypeAdapter.read(inner);
                    V replaced = map.put(key, value);
                    if (replaced != null) {
                        throw new JsonSyntaxException("duplicate key: " + key);
                    }
                    inner.endArray();
                }
                inner.endArray();
            } else {
                inner.beginObject();
                while (inner.hasNext()) {
                    JsonReaderInternalAccess.INSTANCE.promoteNameToValue(inner);
                    K key = keyTypeAdapter.read(inner);
                    V value = valueTypeAdapter.read(inner);
                    V replaced = map.put(key, value);
                    if (replaced != null) {
                        throw new JsonSyntaxException("duplicate key: " + key);
                    }
                }
                inner.endObject();
            }
            return map;
        }

        @Override
        public void write(JsonWriter out, Map<K, V> map) throws IOException {
            if (map == null) {
                out.nullValue();
                return;
            }

            if (!complexMapKeySerialization) {
                out.beginObject();
                out.name("entry");
                out.beginArray();
                for (Map.Entry<K, V> entry : map.entrySet()) {
                    out.beginObject();
                    out.name("key").value(String.valueOf(entry.getKey()));
                    out.name("value");
                    valueTypeAdapter.write(out, entry.getValue());
                    out.endObject();
                }
                out.endArray();
                out.endObject();
                return;
            }

            boolean hasComplexKeys = false;
            List<JsonElement> keys = new ArrayList<JsonElement>(map.size());

            List<V> values = new ArrayList<V>(map.size());
            for (Map.Entry<K, V> entry : map.entrySet()) {
                JsonElement keyElement = keyTypeAdapter.toJsonTree(entry.getKey());
                keys.add(keyElement);
                values.add(entry.getValue());
                hasComplexKeys |= keyElement.isJsonArray() || keyElement.isJsonObject();
            }

            if (hasComplexKeys) {
                out.beginArray();
                for (int i = 0; i < keys.size(); i++) {
                    out.beginArray(); // entry array
                    Streams.write(keys.get(i), out);
                    valueTypeAdapter.write(out, values.get(i));
                    out.endArray();
                }
                out.endArray();
            } else {
                out.beginObject();
                for (int i = 0; i < keys.size(); i++) {
                    JsonElement keyElement = keys.get(i);
                    out.name(keyToString(keyElement));
                    valueTypeAdapter.write(out, values.get(i));
                }
                out.endObject();
            }
        }

        private String keyToString(JsonElement keyElement) {
            if (keyElement.isJsonPrimitive()) {
                JsonPrimitive primitive = keyElement.getAsJsonPrimitive();
                if (primitive.isNumber()) {
                    return String.valueOf(primitive.getAsNumber());
                } else if (primitive.isBoolean()) {
                    return Boolean.toString(primitive.getAsBoolean());
                } else if (primitive.isString()) {
                    return primitive.getAsString();
                } else {
                    throw new AssertionError();
                }
            } else if (keyElement.isJsonNull()) {
                return "null";
            } else {
                throw new AssertionError();
            }
        }
    }
}


Create a new Custom RuntimeWrapper in this way:

package com.tgifridays.ws.gson.typeadapterfactory;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;

/**
 * @author cbelizon
 */
public class CustomTypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> {
    private final Gson context;
    private final TypeAdapter<T> delegate;
    private final Type type;

    CustomTypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) {
        this.context = context;
        this.delegate = delegate;
        this.type = type;
    }

    public T read(JsonReader in) throws IOException {
        return this.delegate.read(in);
    }

    public void write(JsonWriter out, T value) throws IOException {
        TypeAdapter chosen = this.delegate;
        Type runtimeType = this.getRuntimeTypeIfMoreSpecific(this.type, value);
        if (runtimeType != this.type) {
            TypeAdapter runtimeTypeAdapter = this.context.getAdapter(TypeToken.get(runtimeType));
            if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) {
                chosen = runtimeTypeAdapter;
            } else if (!(this.delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) {
                chosen = this.delegate;
            } else {
                chosen = runtimeTypeAdapter;
            }
        }

        chosen.write(out, value);
    }

    private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) {
        if (value != null && (type == Object.class || type instanceof TypeVariable || type instanceof Class)) {
            type = value.getClass();
        }

        return (Type) type;
    }
}


Then create the Gson in this way:

 ConstructorConstructor constructorConstructor = new ConstructorConstructor(new HashMap<Type, InstanceCreator<?>>());
            gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(new TgifMapTypeAdapterFactory(constructorConstructor, false)).create();
Reply all
Reply to author
Forward
0 new messages