Reputation: 503
I have a tree object in JSON format I'm trying to deserialize with Gson. Each node contains its child nodes as fields of object type Node. Node is an interface, which has several concrete class implementations. During the deserialization process, how can I communicate to Gson which concrete class to implement when deserializing the node, if I do not know a priori which type the node belongs to? Each Node has a member field specifying the type. Is there a way to access the field when the object is in serialized form, and somehow communicate the type to Gson?
Thanks!
Upvotes: 50
Views: 40794
Reputation: 360
I was unable to use the TypeAdapter
solutions above, and had almost given up on serializing. In the end I found a solution in an intermediary step between the class object and Gson. I serialized the object itself into a byte array, and then Gson serialized the byte array + deserializing back in similar fashion.
public class GenericSerializer {
public static byte[] serializeForClass(final Object obj) {
try {
final var byteArrayOutputStream = new ByteArrayOutputStream();
final var objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
LOG.warn("Unable to serialize for class '%s'".formatted(obj.getClass().getName()), e);
return null;
}
}
public static <T> T deserializeForClass(final byte[] serializedObj, final Class<T> cls) {
if (serializedObj == null) {
return null;
}
try {
final var inputStream = new ByteArrayInputStream(serializedObj);
final var objectInputStream = new ObjectInputStream(inputStream);
return cls.cast(objectInputStream.readObject());
} catch (ClassNotFoundException | IOException | NullPointerException e) {
LOG.warn("Unable to deserialize for class '%s'".formatted(cls), e);
return null;
}
}
Used like this
final var yourObject = new YourClass();
final var serialized = GenericSerializer.serializeForClass(yourObject);
final var gson = new Gson();
final var gsonSerialized = gson.toJson(serialized);
final var gsonDeserialized = gson.fromJson(gsonSerialized, byte[].class)
final var deserialized = GenericSerializer.deserializeForClass(gsonDeserialized, YourClass.class);
assert(yourObject == deserialized);
I'm hoping this might help someone else the way it helped me.
Upvotes: 0
Reputation: 41
I want to correct the above a little
public class PropertyMarshallerAbstractTask implements JsonSerializer<Object>, JsonDeserializer<Object> {
private static final String CLASS_TYPE = "CLASS_TYPE";
@Override
public Object deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
JsonObject jsonObj = jsonElement.getAsJsonObject();
String className = jsonObj.get(CLASS_TYPE).getAsString();
try {
Class<?> clz = Class.forName(className);
return jsonDeserializationContext.deserialize(jsonElement, clz);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
@Override
public JsonElement serialize(Object object, Type type, JsonSerializationContext jsonSerializationContext) {
Gson gson = new Gson(); //without this line it will not work
gson.toJson(object, object.getClass()); //and this one
JsonElement jsonElement = gson.toJsonTree(object); //it needs to replace to another method...toJsonTree
jsonElement.getAsJsonObject().addProperty(CLASS_TYPE, object.getClass().getCanonicalName());
return jsonElement;
}
}
And then I use it:
Gson gson = new GsonBuilder()
.registerTypeAdapter(AbstractTask.class, new PropertyMarshallerOfAbstractTask())
.create();
And then I can parse List (where I keep some non-abstract classes, which inherited from Abstract Task) to Json;
And it works in the opposite direction
List<AbstractTask> abstractTasks = gson.fromJson(json, new TypeToken<List<AbstractTask>>(){}.getType());
Upvotes: 4
Reputation: 110104
I'd suggest adding a custom JsonDeserializer for Node
s:
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.
Upvotes: 55
Reputation: 13481
You have to use TypeToken
class from Google Gson.
You will need of course has a generic class T to make it works
Type fooType = new TypeToken<Foo<Bar>>() {}.getType();
gson.toJson(foo, fooType);
gson.fromJson(json, fooType);
Upvotes: 0
Reputation: 986
You will need to register both JSONSerializer and JSONDeserializer. Also you can implement a generic adapter for all your interfaces in the following way:
Here is the implementation that I have used for myself and works fine.
public class PropertyBasedInterfaceMarshal implements
JsonSerializer<Object>, JsonDeserializer<Object> {
private static final String CLASS_META_KEY = "CLASS_META_KEY";
@Override
public Object deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException {
JsonObject jsonObj = jsonElement.getAsJsonObject();
String className = jsonObj.get(CLASS_META_KEY).getAsString();
try {
Class<?> clz = Class.forName(className);
return jsonDeserializationContext.deserialize(jsonElement, clz);
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
}
}
@Override
public JsonElement serialize(Object object, Type type,
JsonSerializationContext jsonSerializationContext) {
JsonElement jsonEle = jsonSerializationContext.serialize(object, object.getClass());
jsonEle.getAsJsonObject().addProperty(CLASS_META_KEY,
object.getClass().getCanonicalName());
return jsonEle;
}
}
Then you could register this adapter for all your interfaces as follows
Gson gson = new GsonBuilder()
.registerTypeAdapter(IInterfaceOne.class,
new PropertyBasedInterfaceMarshal())
.registerTypeAdapter(IInterfaceTwo.class,
new PropertyBasedInterfaceMarshal()).create();
Upvotes: 40
Reputation: 5810
As far as I can tell this doesn't work for non-collection types, or more specifically, situations where the concrete type is used to serialize, and the interface type is used to deserialize. That is, if you have a simple class implementing an interface and you serialize the concrete class, then specify the interface to deserialize, you'll end up in an unrecoverable situation.
In the above example the type adapter is registered against the interface, but when you serialize using the concrete class it will not be used, meaning the CLASS_META_KEY data will never be set.
If you specify the adapter as a hierarchical adapter (thereby telling gson to use it for all types in the hierarchy), you'll end up in an infinite loop as the serializer will just keep calling itself.
Anyone know how to serialize from a concrete implementation of an interface, then deserialize using only the interface and an InstanceCreator?
By default it seems that gson will create the concrete instance, but does not set it's fields.
Issue is logged here:
http://code.google.com/p/google-gson/issues/detail?id=411&q=interface
Upvotes: 1