简体   繁体   中英

How to serialize a class with an interface?

I have never done much with serialization, but am trying to use Google's gson to serialize a Java object to a file. Here is an example of my issue:

public interface Animal {
    public String getName();
}


 public class Cat implements Animal {

    private String mName = "Cat";
    private String mHabbit = "Playing with yarn";

    public String getName() {
        return mName;
    }

    public void setName(String pName) {
        mName = pName;
    }

    public String getHabbit() {
        return mHabbit;
    }

    public void setHabbit(String pHabbit) {
        mHabbit = pHabbit;
    }

}

public class Exhibit {

    private String mDescription;
    private Animal mAnimal;

    public Exhibit() {
        mDescription = "This is a public exhibit.";
    }

    public String getDescription() {
        return mDescription;
    }

    public void setDescription(String pDescription) {
        mDescription = pDescription;
    }

    public Animal getAnimal() {
        return mAnimal;
    }

    public void setAnimal(Animal pAnimal) {
        mAnimal = pAnimal;
    }

}

public class GsonTest {

public static void main(String[] argv) {
    Exhibit exhibit = new Exhibit();
    exhibit.setAnimal(new Cat());
    Gson gson = new Gson();
    String jsonString = gson.toJson(exhibit);
    System.out.println(jsonString);
    Exhibit deserializedExhibit = gson.fromJson(jsonString, Exhibit.class);
    System.out.println(deserializedExhibit);
}
}

So this serializes nicely -- but understandably drops the type information on the Animal:

{"mDescription":"This is a public exhibit.","mAnimal":{"mName":"Cat","mHabbit":"Playing with yarn"}}

This causes real problems for deserialization, though:

Exception in thread "main" java.lang.RuntimeException: No-args constructor for interface com.atg.lp.gson.Animal does not exist. Register an InstanceCreator with Gson for this type to fix this problem.

I get why this is happening, but am having trouble figuring out the proper pattern for dealing with this. I did look in the guide but it didn't address this directly.

Here is a generic solution that works for all cases where only interface is known statically.

  1. Create serialiser/deserialiser:

     final class InterfaceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T> { public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) { final JsonObject wrapper = new JsonObject(); wrapper.addProperty("type", object.getClass().getName()); wrapper.add("data", context.serialize(object)); return wrapper; } public T deserialize(JsonElement elem, Type interfaceType, JsonDeserializationContext context) throws JsonParseException { final JsonObject wrapper = (JsonObject) elem; final JsonElement typeName = get(wrapper, "type"); final JsonElement data = get(wrapper, "data"); final Type actualType = typeForName(typeName); return context.deserialize(data, actualType); } private Type typeForName(final JsonElement typeElem) { try { return Class.forName(typeElem.getAsString()); } catch (ClassNotFoundException e) { throw new JsonParseException(e); } } private JsonElement get(final JsonObject wrapper, String memberName) { final JsonElement elem = wrapper.get(memberName); if (elem == null) throw new JsonParseException("no '" + memberName + "' member found in what was expected to be an interface wrapper"); return elem; } } 
  2. make Gson use it for the interface type of your choice:

     Gson gson = new GsonBuilder().registerTypeAdapter(Animal.class, new InterfaceAdapter<Animal>()) .create(); 

Put the animal as transient , it will then not be serialized.

Or you can serialize it yourself by implementing defaultWriteObject(...) and defaultReadObject(...) (I think thats what they were called...)

EDIT See the part about "Writing an Instance Creator" here .

Gson cant deserialize an interface since it doesnt know which implementing class will be used, so you need to provide an instance creator for your Animal and set a default or similar.

@Maciek solution works perfect if the declared type of the member variable is the interface / abstract class. It won't work if the declared type is sub-class / sub-interface / sub-abstract class unless we register them all through registerTypeAdapter() . We can avoid registering one by one with the use of registerTypeHierarchyAdapter , but I realize that it will cause StackOverflowError because of the infinite loop. (Please read reference section below)

In short, my workaround solution looks a bit senseless but it works without StackOverflowError .

@Override
public JsonElement serialize(T object, Type interfaceType, JsonSerializationContext context) {
    final JsonObject wrapper = new JsonObject();
    wrapper.addProperty("type", object.getClass().getName());
    wrapper.add("data", new Gson().toJsonTree(object));
    return wrapper;
}

I used another new Gson instance of work as the default serializer / deserializer to avoid infinite loop. The drawback of this solution is you will also lose other TypeAdapter as well, if you have custom serialization for another type and it appears in the object, it will simply fail.

Still, I am hoping for a better solution.

Reference

According to Gson 2.3.1 documentation for JsonSerializationContext and JsonDeserializationContext

Invokes default serialization on the specified object passing the specific type information. It should never be invoked on the element received as a parameter of the JsonSerializer.serialize(Object, Type, JsonSerializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom serializer again.

and

Invokes default deserialization on the specified object. It should never be invoked on the element received as a parameter of the JsonDeserializer.deserialize(JsonElement, Type, JsonDeserializationContext) method. Doing so will result in an infinite loop since Gson will in-turn call the custom deserializer again.

This concludes that below implementation will cause infinite loop and cause StackOverflowError eventually.

@Override
public JsonElement serialize(Animal src, Type typeOfSrc,
        JsonSerializationContext context) {
    return context.serialize(src);
}

I had the same problem, except my interface was of primitive type ( CharSequence ) and not JsonObject :

if (elem instanceof JsonPrimitive){
    JsonPrimitive primitiveObject = (JsonPrimitive) elem;

    Type primitiveType = 
    primitiveObject.isBoolean() ?Boolean.class : 
    primitiveObject.isNumber() ? Number.class :
    primitiveObject.isString() ? String.class :
    String.class;

    return context.deserialize(primitiveObject, primitiveType);
}

if (elem instanceof JsonObject){
    JsonObject wrapper = (JsonObject) elem;         

    final JsonElement typeName = get(wrapper, "type");
    final JsonElement data = get(wrapper, "data");
    final Type actualType = typeForName(typeName); 
    return context.deserialize(data, actualType);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM