18

I came across some weird behavior in GSON.

If I have the following class structure:

public interface Animal {
    public void nothing();
}

public class Cat implements Animal {
    private String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    public Cat(){}

    @Override
        public void nothing() {
        // TODO Auto-generated method stub
        };
    }

public class Dog implements Animal {
    private String name;

    public Dog(String name) {
            super();
        this.name = name;
    }

    public Dog(){}

    @Override
    public void nothing() {
        // TODO Auto-generated method stub

    };
}

I can do this:

ArrayList<Animal> animals = new ArrayList<Animal>();
    animals.add(new Cat("Betty"));
    animals.add(new Dog("Fred"));
    System.out.println(gson.toJson(animals));

and get this output:

[{"name":"Betty"},{"name":"Fred"}]

However, if I put animals into a containing class:

public class Container {

List<Animal> animals = new ArrayList<Animal>();

public void addAnimal(Animal a){
    animals.add(a);
}
}

and call:

Container container = new Container();
container.addAnimal(new Cat("betty"));
System.out.println(gson.toJson(container));

I get:

{"animals":[{}]}

It looks like GSON can serialize a list of an interface List<Interface> when that list is by itself, but when the list is contained in another class, GSON has problems.

Any idea what I'm doing wrong?

As a side note, I can correctly deserialize a json string into the correct type using a custom deserializer. It's the serializing that is giving me issues.

Thanks

Tyler DeWitt
  • 23,366
  • 38
  • 119
  • 196
  • You can always get the Gson source code and see what they are actually doing in that method. – TS- May 10 '11 at 21:42

3 Answers3

8

It's enough to simply use a custom JsonSerializer that explicitly gets the class of the object (presumably Gson's getting the declared type instead, for some reason). Here's a general solution for serializing Interfaces:

public static class InterfaceSerializer<T> implements JsonSerializer<T> {
    public JsonElement serialize(T link, Type type,
                                 JsonSerializationContext context) {
        // Odd Gson quirk
        // not smart enough to use the actual type rather than the interface
        return context.serialize(link, link.getClass());
    }
}

For your example, I can do the following:

GsonBuilder gbuild = new GsonBuilder();
Gson standard = gbuild.create();

ArrayList<Animal> animals = Lists.newArrayList(new Cat("Betty"),new Dog("Fred"));
System.out.println(standard.toJson(animals));

Container c = new Container();
c.animals.addAll(animals);
System.out.println(standard.toJson(c));

Gson interfaceAware = gbuild
     .registerTypeAdapter(Animal.class, new InterfaceSerializer<>()).create();
System.out.println(interfaceAware.toJson(c));

This outputs:

[{"name":"Betty","hates":"Everything"},{"name":"Fred","loves":"Everything"}]
{"animals":[{},{}]}
{"animals":[{"name":"Betty","hates":"Everything"}, "name":"Fred","loves":"Everything"}]}

The last item being the correctly serialized string.

This isn't enough to deserialize the JSON, unfortunately, because the resulting JSON doesn't contain any type information. Check out this answer to How to serialize a class with an interface? for a way to track the object's type, and therefore serialize/deserialize the object.

Community
  • 1
  • 1
dimo414
  • 47,227
  • 18
  • 148
  • 244
5

It's far from pretty, but the solution I'm using for now is to use

JsonObject jsonObject = gson.toJsonTree(container).getAsJsonObject();

to build a JsonObject. Then I call:

jsonObject.remove("animals");
jsonObject.add("animals",gson.toJsonTree(container.getAnimals()));

and waa laa, the object in correct json form.

Bonus points: I had a list of nested containers, so I had to construct a JsonArray so that I could iterate over my containers and call my custom toJson() on each.

Moral of the story: Add Interface Lists using the

jsonObject.remove();
jsonObject.add(propertyName, property);

trick and iterate over a List of containers using a JsonArray (just using toJson() on the list doesn't call your special method on children containers).

Definitely still looking for a more natural solution.

Happy coding

Tyler DeWitt
  • 23,366
  • 38
  • 119
  • 196
0

The latest release of Gson still behaves as described in the original question. (I think I'll log an enhancement request if one doesn't already exist.)

For what it's worth, Jackson deserializes both example data structures as expected. The Container and Animal implementing classes just need getters for the second example.

// output: {"animals":[{"name":"betty"},{"name":"fred"}]}

import java.io.StringWriter;
import java.util.ArrayList;

import org.codehaus.jackson.map.ObjectMapper;

public class JacksonFoo
{
  public static void main(String[] args) throws Exception
  {
    Container container = new Container();
    container.addAnimal(new Cat("betty"));
    container.addAnimal(new Dog("fred"));
    ObjectMapper mapper = new ObjectMapper();
    StringWriter writer = new StringWriter();
    mapper.writeValue(writer, container);
    System.out.println(writer);
  }
}

class Container
{
  ArrayList<Animal> animals = new ArrayList<Animal>();

  public void addAnimal(Animal a)
  {
    animals.add(a);
  }

  public ArrayList<Animal> getAnimals()
  {
    return animals;
  }
}

interface Animal
{
  public void nothing();
}

class Cat implements Animal
{
  private String name;

  public Cat(String name)
  {
    this.name = name;
  }

  public Cat()
  {
  }

  @Override
  public void nothing()
  {
  }

  public String getName()
  {
    return name;
  }
}

class Dog implements Animal
{
  private String name;

  public Dog(String name)
  {
    this.name = name;
  }

  public Dog()
  {
  }

  @Override
  public void nothing()
  {
  }

  public String getName()
  {
    return name;
  }
}
Programmer Bruce
  • 64,977
  • 7
  • 99
  • 97
  • Thanks for the pointer to Jackson. Next time I play with json I'll take a look at it! – Tyler DeWitt Jun 12 '11 at 18:16
  • 1
    This is a known issue with Gson. Issue 170 was originally logged in October, 2009, regarding it. With an implementation for issue 231 reportedly planned to be included with the next release, this problem may soon be resolved. – Programmer Bruce Jun 19 '11 at 05:05
  • 3
    Alas, it's 2014 and [Issue 231](https://code.google.com/p/google-gson/issues/detail?id=231) remains unresolved :( – dimo414 Apr 20 '14 at 05:11
  • Hi from 2018. Still nothing. – Grecha Aug 09 '18 at 21:01