GSON type adapter: Deserialize Polymorphic Objects Based on “Type” Field
I use Retrofit with default gson parser for JSON processing. Typically, I have a series of 4~5 related but slightly different objects, all of which are subtypes of the public base (which we call “BaseType”). I know we can deserialize the different JSON into their respective submodels by examining the “type” field. The most common prescribed method is to extend JsonDeserializer and register it as a type adapter in a Gson instance:
class BaseTypeDeserializer implements JsonDeserializer<BaseType> {
private static final String TYPE_FIELD = "type";
@Override
public BaseType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonObject() && json.getAsJsonObject().has(TYPE_FIELD)) {
JsonObject jsonObject = json.getAsJsonObject();
final String type = jsonObject.get(TYPE_FIELD).getAsString();
if ("type_a".equals(type)) {
return context.deserialize(json, AType.class);
} else if ("type_b".equals(type)) {
return context.deserialize(json, BType.class);
} ...
If you need to deserialize as BaseType,
deserialize without the current context
or you will infinite loop
return new Gson().fromJson(json, typeOfT);
} else {
Return a blank object on error
return new BaseType();
}
}
}
However, in my experience, this is really slow, it seems to be because we have to load the entire JSON document into JsonElement and then iterate through it to find the type field. I also don’t like that this deserializer has to run on every one of our REST calls, even though the data doesn’t always map to a BaseType (or its children).
This foursquare blog post mentions the use of TypeAdapters as an alternative, But it doesn’t really give further examples.
Does anyone here know how to use TypeAdapterFactory to deserialize based on the “type” field without having to read the entire json stream into the JsonElement object tree?
Solution
- You
should run a custom deserializer only if there are
BaseTypes
or subclasses in the deserialized data, not per request. You register it according to the type, and it is only called when the gson needs to serialize the type.Do you deserialize
BaseType
and subclasses? If so, this line will stifle your performance —return new Gson().fromJson(json, typeOfT);
Creating a new JSON
object isn’t cheap. One is created each time the base class object is deserialized. Moving this call to the constructor of BaseTypeDeserializer
and storing it in a member variable will improve performance (assuming you deserialize the base class).
- The problem with creating a TypeAdapter or
TypeAdapterFactory
to select a type based ona
field is that you need to know the type before you can start using the stream. If the type field is part of the object, you cannot know the type at this time. The post you linked to also mentions —
Deserializers written using TypeAdapters may be less flexible than
those written with JsonDeserializers. Imagine you want a type field to
determine what an object field deserializes to. With the streaming
API, you need to guarantee that type comes down in the response before
object.
You can do
this if you can get the type before the object in the JSON stream, otherwise your TypeAdapter
implementation might reflect your current implementation, except that the first thing you have to do is convert to the JSON tree yourself so you can find the type field. This won’t save you much compared to the current implementation.
If your subclasses are similar and there are no field conflicts between them (fields with the same name but different types), you can use a data transfer object that contains all the fields. Deserialize it using gson, and then use it to create your object.
public class MyDTO { String type; Fields from BaseType String fromBase; Fields from TypeA String fromA; Fields from TypeB // ... } public class BaseType { String type; String fromBase; public BaseType(MyDTO dto) { type = dto.type; fromBase = dto.fromBase; } } public class TypeA extends BaseType { String fromA; public TypeA(MyDTO dto) { super(dto); fromA = dto.fromA; } }
You can then create a TypeAdapterFactory
to handle the conversion from DTOs to your objects
public class BaseTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
if(BaseType.class.isAssignableFrom(type.getRawType())) {
TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
return newItemAdapter((TypeAdapter<BaseType>) delegate,
gson.getAdapter(new TypeToken<MyDTO>(){}));
} else {
return null;
}
}
private TypeAdapter newItemAdapter(
final TypeAdapter<BaseType> delagateAdapter,
final TypeAdapter<MyDTO> dtoAdapter) {
return new TypeAdapter<BaseType>() {
@Override
public void write(JsonWriter out, BaseType value) throws IOException {
delagateAdapter.write(out, value);
}
@Override
public BaseType read(JsonReader in) throws IOException {
MyDTO dto = dtoAdapter.read(in);
if("base".equals(dto.type)) {
return new BaseType(dto);
} else if ("type_a".equals(dto.type)) {
return new TypeA(dto);
} else {
return null;
}
}
};
}
}
And then use it like this –
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new BaseTypeAdapterFactory())
.create();
BaseType base = gson.fromJson(baseString, BaseType.class);