Updated Answer - Best parts of all other answers
I am describing solutions for various use cases and would be addressing the infinite recursion problem as well
Case 1: You are in control of the classes, i.e, you get to write your own Cat
, Dog
classes as well as the IAnimal
interface. You can simply follow the solution provided by @marcus-junius-brutus(the top rated answer)
There won't be any infinite recursion if there is a common base interface as IAnimal
But, what if I don't want to implement the IAnimal
or any such interface?
Then, @marcus-junius-brutus(the top rated answer) will produce an infinite recursion error. In this case, we can do something like below.
We would have to create a copy constructor inside the base class and a wrapper subclass as follows:
.
// Base class(modified)
public class Cat implements IAnimal {
public String name;
public Cat(String name) {
super();
this.name = name;
}
// COPY CONSTRUCTOR
public Cat(Cat cat) {
this.name = cat.name;
}
@Override
public String sound() {
return name + " : \"meaow\"";
};
}
// The wrapper subclass for serialization
public class CatWrapper extends Cat{
public CatWrapper(String name) {
super(name);
}
public CatWrapper(Cat cat) {
super(cat);
}
}
And the serializer for the type Cat
:
public class CatSerializer implements JsonSerializer<Cat> {
@Override
public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {
// Essentially the same as the type Cat
JsonElement catWrapped = context.serialize(new CatWrapper(src));
// Here, we can customize the generated JSON from the wrapper as we want.
// We can add a field, remove a field, etc.
return modifyJSON(catWrapped);
}
private JsonElement modifyJSON(JsonElement base){
// TODO: Modify something
return base;
}
}
So, why a copy constructor?
Well, once you define the copy constructor, no matter how much the base class changes, your wrapper will continue with the same role. Secondly, if we don't define a copy constructor and simply subclass the base class then we would have to "talk" in terms of the extended class, i.e, CatWrapper
. It is quite possible that your components talk in terms of the base class and not the wrapper type.
Is there an easy alternative?
Sure, it has now been introduced by Google - this is the RuntimeTypeAdapterFactory
implementation:
RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(runtimeTypeAdapterFactory)
.create();
Here, you would need to introduce a field called "type" in Animal
and the value of the same inside Dog
to be "dog", Cat
to be "cat"
Complete example: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html
.
// The class we are NOT allowed to modify
public class Dog implements IAnimal {
public String name;
public int ferocity;
public Dog(String name, int ferocity) {
super();
this.name = name;
this.ferocity = ferocity;
}
@Override
public String sound() {
return name + " : \"bark\" (ferocity level:" + ferocity + ")";
}
}
// The marker interface
public interface AnimalInterface {
}
// The subclass for serialization
public class DogWrapper extends Dog implements AnimalInterface{
public DogWrapper(String name, int ferocity) {
super(name, ferocity);
}
}
// The subclass for serialization
public class CatWrapper extends Cat implements AnimalInterface{
public CatWrapper(String name) {
super(name);
}
}
So, we would be using CatWrapper
instead of Cat
, DogWrapper
instead of Dog
and
AlternativeAnimalAdapter
instead of IAnimalAdapter
// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`
public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {
private static final String CLASSNAME = "CLASSNAME";
private static final String INSTANCE = "INSTANCE";
@Override
public JsonElement serialize(AnimalInterface src, Type typeOfSrc,
JsonSerializationContext context) {
JsonObject retValue = new JsonObject();
String className = src.getClass().getName();
retValue.addProperty(CLASSNAME, className);
JsonElement elem = context.serialize(src);
retValue.add(INSTANCE, elem);
return retValue;
}
@Override
public AnimalInterface deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
String className = prim.getAsString();
Class<?> klass = null;
try {
klass = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new JsonParseException(e.getMessage());
}
return context.deserialize(jsonObject.get(INSTANCE), klass);
}
}
We perform a test:
public class Test {
public static void main(String[] args) {
// Note that we are using the extended classes instead of the base ones
IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
Gson gsonExt = null;
{
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
gsonExt = builder.create();
}
for (IAnimal animal : animals) {
String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
System.out.println("serialized with the custom serializer:" + animalJson);
AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
}
}
}
Output:
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}