GSON TypeAdapter:基于“类型”字段反序列化多态对象

kip*_*ip2 5 java android gson json-deserialization retrofit

我正在使用带有默认 Gson 解析器的 Retrofit 进行 JSON 处理。通常,我有一系列 4~5 个相关但略有不同的对象,它们都是公共基础的所有子类型(我们称之为“BaseType”)。我知道我们可以通过检查“类型”字段将不同的 JSON 反序列化为它们各自的子模型。最常用的方法是扩展 JsonDeserializer 并将其注册为 Gson 实例中的类型适配器:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然而,根据我的经验,这真的很慢,似乎是因为我们必须将整个 JSON 文档加载到 JsonElement 中,然后遍历它以找到类型字段。我也不喜欢这个反序列化器必须在我们的每个 REST 调用上运行,即使数据不一定总是映射到 BaseType(或其子项)。

这篇foursquare 博客文章提到使用TypeAdapters 作为替代方法,但它并没有真正通过示例进行进一步说明。

这里有人知道如何使用 TypeAdapterFactory 基于“类型”字段进行反序列化,而不必将整个 json 流读入 JsonElement 对象树?

iag*_*een 5

  1. 自定义反序列化器应该只BaseType在反序列化数据中有 a或 a 子类时才运行,而不是每个请求。您根据类型注册它,并且仅在 gson 需要序列化该类型时才调用它。

  2. BaseType像子类一样反序列化吗?如果是这样,这条线会扼杀你的表现——

    return new Gson().fromJson(json, typeOfT);

创建新Gson对象并不便宜。每次反序列化基类对象时都会创建一个。将此调用移动到 的构造函数BaseTypeDeserializer并将其存储在成员变量中将提高性能(假设您确实反序列化了基类)。

  1. 基于字段创建TypeAdapterTypeAdapterFactory选择类型的问题是您需要在开始使用流之前知道类型。如果类型字段是对象的一部分,则此时您无法知道类型。你链接到的帖子提到了很多——

使用 TypeAdapters 编写的反序列化器可能不如使用 JsonDeserializers 编写的灵活。想象一下,您想要一个类型字段来确定对象字段反序列化为什么。使用流式 API,您需要保证类型在对象之前的响应中出现。

如果你能在 JSON 流中获得对象之前的类型,你就可以做到,否则你的TypeAdapter实现可能会反映你当前的实现,除了你做的第一件事是你自己转换成 Json 树,这样你才能找到类型场地。与当前的实现相比,这不会为您节省太多。

  1. 如果您的子类相似,并且它们之间没有任何字段冲突(名称相同但类型不同的字段),则可以使用包含所有字段的数据传输对象。使用 gson 对其进行反序列化,然后使用它创建您的对象。

    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;
      }
    }
    
    Run Code Online (Sandbox Code Playgroud)

然后你可以创建一个TypeAdapterFactory处理从 DTO 到你的对象的转换——

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;
        }
      }
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

像这样使用——

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(new BaseTypeAdapterFactory())
    .create();

BaseType base = gson.fromJson(baseString, BaseType.class);
Run Code Online (Sandbox Code Playgroud)