user291701
user291701

Reputation: 39681

Deserializing an interface using Gson?

I'm trying to use Gson with an interface:

public interface Photo {
    public int getWidth();
}

public class DinosaurPhoto implements Photo {
    ...
}

public class Wrapper {
    private Photo mPhoto; // <- problematic
}

... 

Wrapper wrapper = new Wrapper();
wrapper.setPhoto(new DinosaurPhoto());
Gson gson = new Gson();
String raw = gson.toJson(wrapper);  

// Throws an error since "Photo" can't be deserialized as expected.
Wrapper deserialized = gson.fromJson(raw, Wrapper.class);

Since the Wrapper class has a member variable that is of type Photo, how do I go about deserializing it using Gson?

Thanks

Upvotes: 9

Views: 9527

Answers (3)

Programmer Bruce
Programmer Bruce

Reputation: 66943

Custom deserialization is necessary.

Depending on the larger problem to be solved, either a ["type adapter"] 1 or a "type hierarchy adapter" should be used. The type hierarchy adapter "is to cover the case when you want the same representation for all subtypes of a type".

Upvotes: 8

jnorthrup
jnorthrup

Reputation: 33

I have built a primitive interface shim generator by way of compiling a groovy properties class to interoperate with a GWT Autobeans model. this is a really rough method to sidestep the ASM/cglib learning curve for now. background on this: with Autobeans, you may only use interfaces, and the sun.* proxies are incapable of gson interop for all the access attempts I have experimented with. BUT, when groovy classloader is local to GsonBuilder, things get a tiny bit easier. note, this fails unless the gsonBuilder registration is actually called from within the groovy itself.

to access the shim factory create one as a singleton names JSON_SHIM and call

JSON_SHIM.getShim("{}",MyInterface.class)

to register if needed and create a [blank] instance. if you have interfaces in your interfaces, you must pre-register those too ahead of use. this is just enough magic to use flat Autobeans with gson, not a whole framework. there is no groovy code in this generator, so someone with javassist-foo can repeat the experiment.

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;
  }
}

Upvotes: 0

Willem
Willem

Reputation: 376

Simply put, you can't do that with GSON.

I was troubled by the same problem when I stumbled upon Jackson. With it it is very easy:

ObjectMapper mapper = new ObjectMapper();  
mapper.enableDefaultTyping(); 

And then you can go about de/serializing your Java objects and interfaces without having to write additional custom de/serializers, annotaions and really no added code whatsoever.

This was not a part of the question, but may prove useful if you decide to port from Gson to Jackson. Gson supports private fields by default but for Jackson you have to include this in your code.

mapper.setVisibilityChecker(g.getVisibilityChecker().with(Visibility.ANY));

Sample implementation for your code in main:

ObjectMapper mapper = new ObjectMapper();  
mapper.enableDefaultTyping();
mapper.setVisibilityChecker(g.getVisibilityChecker().with(Visibility.ANY));
Wrapper wrapper = new Wrapper();
wrapper.setPhoto(new DinosaurPhoto());
String wrapper_json = mapper.writeValueAsString(wrapper);
Wrapper wrapper_from_json = mapper.readValue(wrapper_json,Wrapper.class);

Gson promised they will work on this problem in future versions, but they haven't solved it so far. If this is very important for you application I would suggest that you port to Jackson.

Upvotes: 5

Related Questions