Java – GSON type adapter: Deserialize Polymorphic Objects Based on “Type” Field

GSON type adapter: Deserialize Polymorphic Objects Based on “Type” Field… here is a solution to the problem.

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

  1. 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.

  2. 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).

  1. The problem with creating a TypeAdapter or TypeAdapterFactory to select a type based on a 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.

  1. 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);

Related Problems and Solutions