38

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.

Jonas
  • 121,568
  • 97
  • 310
  • 388
Ben Flynn
  • 18,524
  • 20
  • 97
  • 142

4 Answers4

90

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();
    
narthi
  • 2,188
  • 1
  • 16
  • 27
  • 8
    StackOverflow exception -- context.serialize(object) calls itself. Don't know how that worked for you. – Ryan Dec 03 '13 at 02:27
  • @maciek-makowski This code is simple and beautiful, do you know where it came from ? If this is yours, what's the license ? Thanks – Jeremy Rocher Oct 29 '14 at 15:10
  • @JeremyRocher the code is mine. Per the StackOverflow footnote it is "licensed under cc by-sa 3.0 with attribution required". I actually consider it public domain, but I do not know what legal effect this statement has in the legal regime you're under. – narthi Oct 30 '14 at 21:43
  • Thank you for precising it, the license will give details by itself. – Jeremy Rocher Nov 16 '14 at 19:17
  • It works great for me. But, I don't see how it works, giving what @Ryan pointed out. – Connor Clark Dec 22 '14 at 07:00
  • 1
    @Hoten: this works because GSON takes into account both the declared type and actual type when choosing the serialiser. When field has type `Animal` GSON will prefer the type adapter for `Animal` interface, if such has been registered, even if actual type is `Cat`. In the `context.serialize(object)` call, however, there is no "declared type" to consider, it's just a `Cat` that is passed, so the type adapter will not be used. – narthi Jan 04 '15 at 21:00
  • Ah, I see. Very confusing to reason about, at least for me. Thanks for clearing that up. – Connor Clark Jan 09 '15 at 22:02
  • 6
    @Ryan is right, context.serailize(object) ends up in infinite loop - how did it work for rest of you guys? – Srneczek Sep 04 '16 at 21:27
  • In my testing, this works if the implementation/subclass object is contained in another object (per the question) but not if it is freestanding. – stevehs17 Aug 16 '17 at 23:48
  • This is a great solution, but it could be improved. This serializes the entire `type` name and hence the full type name is visible in the JSON string. This looks bad and exposes the data model package details. Instead use `object.getClass().getSimpleName()` while serializing, and add the full package name while deserializing. The JSON string will look clean. – Henry Sep 25 '17 at 04:42
  • 10
    This doesn't seem to work for me. I get `com.google.gson.JsonParseException: no 'type' member found in json file.` – Robert Oct 18 '17 at 07:39
  • 4
    I am getting this error `com.google.gson.JsonParseException: no 'type' member found in what was expected to be an interface wrapper` – Rajeev Shetty Dec 04 '19 at 13:34
  • 1
    You must not use this code for untrusted data; `Class.forName` allows loading arbitrary classes which can be abused by an attacker (see related [ysoserial](https://github.com/frohoff/ysoserial)). Instead you should maintain a mapping from known type name to class; this would also allow using arbitrary IDs for types and would make the JSON data programming language independent. – Marcono1234 May 31 '21 at 13:01
14

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.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
rapadura
  • 5,242
  • 7
  • 39
  • 57
  • 1
    I do want Animal to be serialized / deserialized. Could you be more specific about those Write and Read methods? Thanks. – Ben Flynn Jan 25 '11 at 15:39
  • Sorry I was thinking of the standard serializer methods to override. I see gson has http://google-gson.googlecode.com/svn/trunk/gson/docs/javadocs/com/google/gson/JsonDeserializer.html which you can try, you can override the default deserializationw it it. – rapadura Jan 25 '11 at 16:53
  • I think I need more than a deserializer because the Animal isn't being serialized with any type information. Suppose the type information were there, who would take up the responsibility for deserializing the implementing classes? – Ben Flynn Jan 25 '11 at 17:55
  • I am not sure. Serializing works since you pass it in an actual class, Cat, but deserialization fails since the Exhibition class only has an Animal, gson doesnt know which implementing class to revive. You could implement the JsonDeserializer for Animal, where you could provide your own logic to make either a Cat or Dog or something else. That solution is not very beautiful, but it does hint that you should only serialize pure data classes, not classes involved in inheritance and polymorphism. – rapadura Jan 26 '11 at 09:06
  • Another way to go at it is to define a JsonSerializer for Exhbition, and serialize it, then look at what kind of Animal it has, then write mCat or mDog instead of mAnimal in the serialized form. Then deserialization should be easier perhaps. – rapadura Jan 26 '11 at 09:21
  • 1
    Yes I ended up basically doing that -- having the serialized for Exhibition extract the class implementing Animal, serializing the class name, then calling the appropriate serializer on that class to serialize the data. When I deserialize I then use reflection to instantiate the appropriate deserializer. I'm pretty convinced I made it more complicated than it needs to be, but on the other hand, it works. =) – Ben Flynn Mar 07 '11 at 15:55
4

@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);
}
Victor Wong
  • 2,486
  • 23
  • 32
-1

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);
}
anat
  • 1