Deserializing an Abstract Class in Gson

Deserializing an abstract class in Gson

I'd suggest adding a custom JsonDeserializer for Nodes:

Gson gson = new GsonBuilder()
.registerTypeAdapter(Node.class, new NodeDeserializer())
.create();

You will be able to access the JsonElement representing the node in the deserializer's method, convert that to a JsonObject, and retrieve the field that specifies the type. You can then create an instance of the correct type of Node based on that.

Gson and abstract superclasses: deserialization issue

Following the Axxiss link, here follows the answer. A custom serializer/deserializer must be provided.

public class AClassAdapter  implements JsonSerializer<A>, JsonDeserializer<A> {
@Override
public JsonElement serialize(A src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
result.add("type", new JsonPrimitive(src.getClass().getSimpleName()));
result.add("properties", context.serialize(src, src.getClass()));
return result;
}

@Override
public A deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject jsonObject = json.getAsJsonObject();
String type = jsonObject.get("type").getAsString();
JsonElement element = jsonObject.get("properties");

try {
String fullName = typeOfT.getTypeName();
String packageText = fullName.substring(0, fullName.lastIndexOf(".") + 1);

return context.deserialize(element, Class.forName(packageText + type));
} catch (ClassNotFoundException cnfe) {
throw new JsonParseException("Unknown element type: " + type, cnfe);
}
}
}

Then the serialization is done like follows:

GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(A.class, new ATypeAdapter());
String json = gson.create().toJson(list);

and given the json string, the deserialization is:

GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(A.class, new ATypeAdapter());

return gson.create().fromJson(json, A[].class);

Abstract class with Gson serialization/deserialization

Thanks to @fluffy's suggestion, I managed to skip writing a custom deserialiser and making use of RuntimeTypeAdapterFactory from gson-extras library:

public static void writeObjectToFile(Object object, String filePath) {
RuntimeTypeAdapterFactory<Parameter> parameterAdapterFactory = RuntimeTypeAdapterFactory.of(Parameter.class, "type");
parameterAdapterFactory.registerSubtype(StringParameter.class, "StringParameter");
parameterAdapterFactory.registerSubtype(PasswordParameter.class, "PasswordParameter");
Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(parameterAdapterFactory).create();
try {
FileWriter fileWriter = new FileWriter(filePath);
gson.toJson(object, fileWriter);
fileWriter.flush();
fileWriter.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}

public static LinkedList<Request> readObjectFromFile(String filePath) {
RuntimeTypeAdapterFactory<Parameter> parameterAdapterFactory = RuntimeTypeAdapterFactory.of(Parameter.class, "type");
parameterAdapterFactory.registerSubtype(StringParameter.class, "StringParameter");
parameterAdapterFactory.registerSubtype(PasswordParameter.class, "PasswordParameter");
Gson gson = new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(parameterAdapterFactory).create();
try {
BufferedReader jsonBuffer = new BufferedReader(new FileReader(filePath));
return gson.fromJson(jsonBuffer, new TypeToken<LinkedList<Request>>(){}.getType());
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
}
return null;
}

I also kept only the lombok annotations and did not write constructors and getters/setters for my POJOs

Custom gson serialization for an abstract class

How can I read this JSON as List?

One possibility would be to create a custom deserializer that acts like a factory.

The first step would be to define this deserializer

class PersonJsonDeserializer implements JsonDeserializer<Person> {
@Override
public Person deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String type = json.getAsJsonObject().get("type").getAsString();
switch(type) {
case "zombie":
return context.deserialize(json, Zombie.class);
case "hunter":
return context.deserialize(json, Hunter.class);
default:
throw new IllegalArgumentException("Neither zombie or hunter");
}
}
}

It fetches the value associated with the key "type" and choose the proper type to deserialize the object you're currently reading.

Then, you need to plug this deserializer within the parser.

public class GsonTest {
public static void main(String[] args) {
String json = "[\n" +
" {\"id\":1, \"type\":\"zombie\", \"name\":\"Ugly Tom\", \"uglyness\":42},\n" +
" {\"id\":2, \"type\":\"hunter\", \"name\":\"Shoot in leg Joe\", \"skill\":0}\n" +
"]";

Gson gson = new GsonBuilder().registerTypeAdapter(Person.class, new PersonJsonDeserializer()).create();

Type type = new TypeToken<List<Person>>(){}.getType();

List<Person> list = gson.fromJson(json, type);

for(Person p : list) {
System.out.println(p);
}
}
}

Running it with your example, I get:

Zombie{id=1; name=Ugly Tom; uglyness=42}
Hunter{id=2; name=Shoot in leg Joe; skill=0}

If the value of the type already corresponds to the class name, you might want to use Class.forName also:

class PersonJsonDeserializer implements JsonDeserializer<Person> {
@Override
public Person deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String className = json.getAsJsonObject().get("type").getAsString();
className = Character.toUpperCase(className.charAt(0)) + className.substring(1);
try {
return context.deserialize(json, Class.forName(className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

Deserializing an abstract class in Gson

I'd suggest adding a custom JsonDeserializer for Nodes:

Gson gson = new GsonBuilder()
.registerTypeAdapter(Node.class, new NodeDeserializer())
.create();

You will be able to access the JsonElement representing the node in the deserializer's method, convert that to a JsonObject, and retrieve the field that specifies the type. You can then create an instance of the correct type of Node based on that.

Gson: Deserialize Json into Abstract Class

You'd need to create something like a Gson TypeAdapter and register it with your Gson instance.

I'm not sure how/whether this will work for your particular data format, but here's an example that I've used in my own projects:

public class TimeZoneAdapter extends TypeAdapter<TimeZone> {
@Override
public void write(JsonWriter out, TimeZone value) throws IOException {
out.value(value.getID());
}

@Override
public TimeZone read(JsonReader in) throws IOException {
return TimeZone.getTimeZone(in.nextString());
}
}

You would then register it when building a Gson instance like so:

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(TimeZone.class, new TimeZoneAdapter());

Gson gson = builder.create();

Hope this helps!

Gson deserialization of a multimap with abstract classes

I don't really know a way to do this with gson without it being manually or hacky. This is too big to put as a comment, so I'll leave it here as an answer as an idea to help you out.

First, you get a stack overflow because you are calling context.deserialize on the same parameters which triggers gson to call the same deserializer, which will call again context.deserialize and so on, until the stack overflow.

You'll run into the same problem when serializing because you're also just doing context.serialize.

To avoid this, you'll need to avoid that gson recurses into calling the serializer's/deserializer's methods. This is very easy to achieve by creating another gson instance without the adapters:

public class PetAdapter 
implements JsonSerializer<Pet>, JsonDeserializer<Pet> {
private final Gson gson = new Gson();

@Override
public Pet deserialize(JsonElement jsonElement, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
EntityType entityType = EntityType.valueOf(jsonElement.getAsJsonObject().get("entityType").getAsString());

switch (entityType) {
case IRON_GOLEM:
return gson.fromJson(jsonElement, EcoPet.class);
case WOLF:
return gson.fromJson(jsonElement, BoostPet.class);
case MAGMA_CUBE:
return gson.fromJson(jsonElement, CombatPet.class);
default:
throw new JsonParseException("Invalid PetType");
}
}

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

This works, but only if your Pet implementations do not depend on other custom serializers/deserializers. So as you can imagine this is quite hacky.

Another approach is the manual deserialization. This means that you'd have to go through json element and read the properties like you are reading entityType and manually build your objects.

Very similarly, I guess (I didn't check this), you could first use context to deserialize each pet into a Map of objects and let each pet implement a static method that creates an instance of the specific pet from this map. Something like:

public class IronGolem extends Pet {
public static IronGolem from(Map<String, Object> deserializedPet) {
// here check the map for each thing you need
return new IronGolem(/*pass in every attribute*/);
}
}

Hope this helps.



Related Topics



Leave a reply



Submit