GSON deserialization with generic types and generic field names
One idea would be to define a custom generic deserializer. Its generic type will represent the concrete class of the list's elements wrapped in a Body
instance.
Assuming the following classes:
class Body<T> {
private List<T> list;
public Body(List<T> list) {
this.list = list;
}
}
class Cat {
private int id;
private String title;
...
}
class Truck {
private int id;
private String engine;
private int wheels;
...
}
The deserializer assumes that the structure of the json is always the same, in the sense that you have an object that contains an object named "body". Also it assumes that the value in the first key-value pair of this body is a list.
Now for each element in the json array, we need again to fetch the inner object associated with each key. We deserialize this value and put it in the list.
class CustomJsonDeserializer<T> implements JsonDeserializer<Body<T>> {
private final Class<T> clazz;
public CustomJsonDeserializer(Class<T> clazz) {
this.clazz = clazz;
}
@Override
public Body<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject body = json.getAsJsonObject().getAsJsonObject("body");
JsonArray arr = body.entrySet().iterator().next().getValue().getAsJsonArray();
List<T> list = new ArrayList<>();
for(JsonElement element : arr) {
JsonElement innerElement = element.getAsJsonObject().entrySet().iterator().next().getValue();
list.add(context.deserialize(innerElement, clazz));
}
return new Body<>(list);
}
}
For the final step, you just need to create the corresponding type, instantiate and register the adapter in the parser. For instance for trucks:
Type truckType = new TypeToken<Body<Truck>>(){}.getType();
Gson gson = new GsonBuilder()
.registerTypeAdapter(truckType, new CustomJsonDeserializer<>(Truck.class))
.create();
Body<Truck> body = gson.fromJson(new FileReader("file.json"), truckType);
You can even return the list directly from the adapter if you want to get rid of the Body
class.
With the trucks you'll get [1_big_12, 2_super big_128]
as output and [1_cat1, 2_cat2]
with the cats.
I would be using this approach:
public class Body {
private List<Animal> animals;
}
}
public class Animal {}
public class Dog extends Animal {}
public class Cat extends Animal {}
In this case you'll have serialization w/o any boilerplates, except fact that you have to use Gson TypeAdapter
for Animal
class, like:
Gson gson = new GsonBuilder()
.registerTypeAdapter(Animal.class, new AnimalSerializer())
.create();
Where TypeAdapter should look smth like:
public class AnimalSerializer implements JsonSerializer<Animal>, JsonDeserializer<Animal> {
private static final String CLASS_META_KEY="clz";
@Override
public JsonElement serialize(Animal src, Type typeOfSrc,
JsonSerializationContext context) {
JsonElement element=null;
if (src == null) {
return element;
}
if(src instanceof Cat)
element = context.serialize(src, Cat.class);
else if(src instanceof Dog)
element = context.serialize(src, Dog.class);
else
throw new IllegalArgumentException("Unspecifiad class serializer for "+src.getClass().getName());
element.getAsJsonObject().addProperty(CLASS_META_KEY, src.getClass().getCanonicalName());
return element;
}
@Override
public Field deserialize(JsonElement jsonElement, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
Class<?> clz;
Animal animal;
JsonObject object = jsonElement.getAsJsonObject();
if (object.has(CLASS_META_KEY)) {
String className = object.get(CLASS_META_KEY).getAsString();
try {
clz = Class.forName(className);
} catch (Exception e) {
Log.e(TAG, "Can't deserialize class="+className,e);
clz = Animal.class;
}
animal = context.deserialize(jsonElement, clz);
} else {
animal = context.deserialize(jsonElement, typeOfT);
}
return animal;
}
}