Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gson does not deserialize interfaces, even with an InstanceCreator #411

Open
GoogleCodeExporter opened this issue Mar 19, 2015 · 15 comments

Comments

@GoogleCodeExporter
Copy link

What steps will reproduce the problem?

1. Create an interface that defines getters only
2. Create a concrete class that implements the interface, with local fields and 
accessors corresponding to the interface
3. Specify an InstanceCreator to create concrete instances of the interface
4. Serialize a concrete instance of the class using gson
5. Deserialize from the JSON string produced in step 4.

What is the expected output? What do you see instead?

Given that an InstanceCreator is specified, I would expect a concrete 
implementation of the interface corresponding to the type created in the 
InstanceCreator, with the fields set from the JSON string.

Instead, a concrete instance of the class is returned but the fields are NOT 
populated.

Using version 2.1

I get that this can be done by creating a custom serializer/deserializer for 
the type, but I don't get why that should be required.  If so I can just use a 
regular JSONObject and do it all myself.  Seems like it should be a fairly 
simple task.  Take the runtime type created by the InstanceCreator, match the 
fields in the instance against the values in the JSON string using reflection, 
set and repeat.

Am I missing something here?

Original issue reported on code.google.com by jason.po...@gmail.com on 21 Feb 2012 at 2:33

@GoogleCodeExporter
Copy link
Author

The core problem is that we inspect the fields of the declared type, not the 
InstanceCreator-created type.

Original comment by limpbizkit on 11 Apr 2012 at 8:50

  • Changed state: Accepted

@GoogleCodeExporter
Copy link
Author

if the interface defines setters has the same problem anyway.

Original comment by roc...@gmail.com on 18 Apr 2012 at 3:35

@GoogleCodeExporter
Copy link
Author

This fails in 2.1 and 2.0.
Works fine in 1.7.2 though.

Original comment by future...@gmail.com on 30 Apr 2012 at 10:03

@GoogleCodeExporter
Copy link
Author

Same problem.

"The core problem is that we inspect the fields of the declared type, not the 
InstanceCreator-created type"

Can this be solved and more important, will it be solved?

Original comment by wd.dewi...@gmail.com on 18 Jul 2012 at 8:40

@GoogleCodeExporter
Copy link
Author

The same problem exists with abstract classes (and not only interfaces)

Original comment by ecoffet....@gmail.com on 24 Aug 2012 at 1:28

@GoogleCodeExporter
Copy link
Author

You can work around this problem by defining your own TypeAdapter for the 
interface type.

Original comment by limpbizkit on 2 Sep 2012 at 9:43

@GoogleCodeExporter
Copy link
Author

multiple TypeAdapter cause infinite loop....why it is so??

Original comment by bhavesh4...@gmail.com on 18 Oct 2012 at 7:29

@GoogleCodeExporter
Copy link
Author

I have something that you may want to pick up and run with.  

im using gwt autobeans and had to figure out a way to make gson work around 
incompatible proxies on the server side. what works is to use groovy to be the 
enclosing classloader during TypeAdapter registration and for groovy to copmile 
the adapter class and register it.  the generation code can be cleaned up and 
some references to my gson singletions exist here.  i think the generated 
fromJson method is now superfluous as well. this is the only groovy in my 
project, fwiw, using pure java syntax, so cleaner solutions exist with bytecode 
engineering, but this one was an easy sample to plug in.

cheers. 


import com.google.gson.GsonBuilder;
import com.google.gson.InstanceCreator;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
import groovy.lang.GroovyClassLoader;
import org.apache.commons.beanutils.PropertyUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;

public class GroovyGsonShimFactory {
  private  Map<Class, Method> shimMethods = new LinkedHashMap<>();

  private void generateGroovyProxy(Class ifaceClass) {
    String shimClassName = ifaceClass.getSimpleName() + "$Proxy";
    String ifaceClassCanonicalName = ifaceClass.getCanonicalName();
    String s = "import com.google.gson.*;\n" +
        "import org.apache.commons.beanutils.BeanUtils;\n" +
         "import java.lang.reflect.*;\n" +
        "import java.util.*;\n\n" +
        "public class "+shimClassName+" implements "+ifaceClassCanonicalName+" {\n" ;

    {
      PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(ifaceClass);
      for (PropertyDescriptor p : propertyDescriptors) {
        String name = p.getName();
        String tname = p.getPropertyType().getCanonicalName();
        s += "public " + tname + " " + name + ";\n";
        s += " " + p.getReadMethod().toGenericString().replace("abstract", "").replace(ifaceClassCanonicalName + ".", "") + "{return " + name + ";};\n";
        Method writeMethod = p.getWriteMethod();
        if (writeMethod != null)
          s += " " + writeMethod.toGenericString().replace("abstract", "").replace(ifaceClassCanonicalName + ".", "").replace(")", " v){" + name + "=v;};") + "\n\n";
      }
    }
    s+=        "  public static "+ifaceClassCanonicalName+" fromJson(String s) {\n" +
        "    return (" +ifaceClassCanonicalName+
        ")cydesign.strombolian.server.ddl.DefaultDriver.gson().fromJson(s, "+shimClassName+".class);\n" +
        "  }\n" +  
        "  static public interface foo extends InstanceCreator<"+ifaceClassCanonicalName+">, JsonSerializer<"+ifaceClassCanonicalName+">, JsonDeserializer<"+ifaceClassCanonicalName+"> {}\n" +
        "  static {\n" +
        "    cydesign.strombolian.server.ddl.DefaultDriver.builder().registerTypeAdapter("+ifaceClassCanonicalName+".class, new foo() {\n" +
        "      public "+ifaceClassCanonicalName+" deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {\n" +
        "        return context.deserialize(json, "+shimClassName+".class);\n" +
        "      }\n" +
        "\n" +
        "      public "+ifaceClassCanonicalName+" createInstance(java.lang.reflect.Type type) {\n" +
        "        try {\n" +
        "          return new "+shimClassName+"();\n" +
        "        } catch (Exception e) {\n" +
        "          e.printStackTrace(); \n" +
        "        }\n" +
        "        return null;\n" +
        "      }\n" +
        "\n" +
        "      @Override\n" +
        "      public JsonElement serialize("+ifaceClassCanonicalName+" src, Type typeOfSrc, JsonSerializationContext context) {\n" +
        "        LinkedHashMap linkedHashMap = new LinkedHashMap();\n" +
        "        try {\n" +
        "          BeanUtils.populate(src, linkedHashMap);\n" +
        "          return context.serialize(linkedHashMap);\n" +
        "        } catch (Exception e) {\n" +
        "          e.printStackTrace(); \n" +
        "        }\n" +
        "\n" +
        "        return null;\n" +
        "      }\n" +
        "    });\n" +
        "  }\n\n" +
        "};";

    System.err.println("" + s);
    ClassLoader parent = DefaultDriver.class.getClassLoader();
    GroovyClassLoader loader = new GroovyClassLoader(parent);

    final Class gClass = loader.parseClass(s);
    try {
      Method shimMethod = gClass.getMethod("fromJson", String.class);
      shimMethods.put(ifaceClass, shimMethod);
    } catch (NoSuchMethodException e) {
      e.printStackTrace();
    }

  }

  public <T> T getShim(String json, Class<T> ifaceClass) {
    if (!shimMethods.containsKey(ifaceClass))
      generateGroovyProxy(ifaceClass);
    T shim = null;//= gson().shimMethods(json, CowSchema.class);
    try {
      shim = (T) shimMethods.get(ifaceClass).invoke(null, json);
    } catch (IllegalAccessException | InvocationTargetException e) {
      e.printStackTrace(); 
    }
    return shim;
  }
}

Original comment by northrup...@gmail.com on 5 Feb 2014 at 5:31

@GoogleCodeExporter
Copy link
Author

Is this fixed in any version?

Original comment by mrinmoy....@ideacrestsolutions.com on 11 Feb 2015 at 4:15

@wtfiwtz
Copy link

wtfiwtz commented Jul 14, 2015

The only solutions I have found were to do a TypeAdapter as discussed (slow), or just clone the object into another proxy object that serializes exactly as you want. Its a bit of messing about though, but that is a faster solution.

@volocopter
Copy link

This is a major bug. I have wasted 2 days into this till I came across this page. Google, please solve this problem or at least give an easier workaround

@bes1002t
Copy link

bes1002t commented Mar 9, 2016

This issue schould be prioritized. I need it soon 👍

@elye
Copy link

elye commented Apr 17, 2018

@erik777
Copy link

erik777 commented Apr 23, 2018

I can't even get this to work with a TypeAdaptor. It is complicated by the fact that the interface is implemented by an enum, which you cannot instantiate yourself. When I try to work around by having the TypeAdaptor return the enum value, it errors with:

Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 13 path $.doctype

So, I don't even have a work-around.

@Marcono1234
Copy link
Collaborator

A workaround for this can be to use a custom TypeAdapterFactory, such as the following one:

class SubtypeDeserializingAdapterFactory implements TypeAdapterFactory {
  private final Map<Class<?>, Class<?>> subtypeMap;

  public SubtypeDeserializingAdapterFactory(Map<Class<?>, Class<?>> subtypeMap) {
    this.subtypeMap = subtypeMap;
  }

  @Override
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    Class<?> raw = type.getRawType();
    Class<?> mappedType = subtypeMap.get(raw);

    if (mappedType == null) {
      return null;
    }

    final TypeAdapter<T> serializationDelegate = gson.getDelegateAdapter(this, type);
    @SuppressWarnings("unchecked")
    final TypeAdapter<T> deserializationDelegate = (TypeAdapter<T>) gson.getAdapter(mappedType);

    return new TypeAdapter<T>() {
      @Override
      public void write(JsonWriter out, T value) throws IOException {
        serializationDelegate.write(out, value);
      }

      @Override
      public T read(JsonReader in) throws IOException {
        return deserializationDelegate.read(in);
      }
    };
  }
}

You can then define the mapping from interface or abstract class to implementation class like this:

interface MyInterface {

}
static class MyImplClass implements MyInterface {
  int i;

  @Override
  public String toString() {
    return "MyImplClass[" + i + "]";
  }
}

public void test() {
  Map<Class<?>, Class<?>> subtypeMap = new HashMap<>();
  // Defines deserialization mapping from MyInterface -> MyImplClass
  subtypeMap.put(MyInterface.class, MyImplClass.class);
  Gson gson = new GsonBuilder()
      .registerTypeAdapterFactory(new SubtypeDeserializingAdapterFactory(subtypeMap))
      .create();
  System.out.println(gson.fromJson("{\"i\":2}", MyInterface.class));
}

Note that this likely prevents the preference order of TypeAdapterRuntimeTypeWrapper from working correctly for adapters created by that factory (it would always prefer the adapter for the runtime type, even if that uses reflection), but that is most likely not an issue in most situations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants