【问题标题】:Adapting Retrofit responses using GSON使用 GSON 调整改造响应
【发布时间】:2015-07-18 20:04:12
【问题描述】:

我希望通过从特定 API 收到的每个成功响应来检索已知 JSON 对象的子元素。

每个服务器响应都返回以下 JSON 格式(为简单起见进行了精简):

{
    "status": "success",
    "error_title": "",
    "error_message": "",
    "data": {
        "messages": [
            { "message_id": "123",
              "content": "This is a message" },
            { "message_id": "124",
              "content": "This is another message" }
        ]
    }
}

错误响应包含相同的通用格式,“数据”对象为空,错误相关的 JSON 对象包含有用的值。在出现错误的情况下,我想提取与错误相关的 JSON 对象。

通过上述响应,我有一个 MessageResponse 类,其中包含状态、errorTitle 和 errorMessage 字符串属性以及一个 MessageData 对象。然后MessageData 对象包含一个消息列表 - List<Message> messages。我在这种情况下获取消息的 GET 方法如下(为简单起见):

@GET("/chat/conversation")
void getMessages(Callback<MessageResponse> callback);

如果我坚持使用 GSON 的序列化程序提供的开箱即用的默认 POJO 映射,则此设计需要三个类用于 每个 响应类型。我的最终目标是通过仅从成功的服务器响应中读取我需要的内容并忽略其余部分来减少所需的类数量。我希望此 API 上的所有成功回调数据类型尽可能接近“数据”内容。

换句话说,我想不可知地返回“数据”的子元素。在上面的例子中,它是一个名为“messages”的数组,但在其他一些响应中,它可能是一个“用户”对象,例如。我知道这可以通过为每种响应类型注册单独的 TypeAdapters 来完成,但我想通过使用单一的通用解决方案来实现我的最终目标。

更新:从下面执行大卫的建议

public class BaseResponse<T> {
     @SerializedName("status") public String status;
     @SerializedName("error_title") public String errorMessageTitle;
     @SerializedName("error_message") public String errorMessage;
     @SerializedName("data") public T data;
}

public class MessagesResponse extends BaseResponseData<List<Message>> {
     @SerializedName("messages") List<Message> messages;
}

@GET("/chat/conversation")
void getMessages(Callback<BaseResponse<MessageResponse>> callback);

不幸的是,这没有得到正确的序列化。如果我能以某种方式通知 GSON 来自“数据”父级的可变名称 JSON 对象子级,并将该子级反序列化为由通用数据类型引用的模型类。本质上,dataJsonObject.getChild()

【问题讨论】:

  • 所以你有一个固定的 JSON 结构,但data 可能是任何东西?或者只是许多预定义类型中的任何一种?如果后者是真的,我使用了一个通用的BaseResponse&lt;T&gt;,其中data 的类型是T。然后,例如,一个 UserResponse extends BaseResponse&lt;User&gt; 只需要实现一个构造函数并从 BaseResponse 继承所有其他内容。
  • @david.mihola 感谢您的好主意。我尝试将我的模型设置与您提出的设计相匹配,但我收到了一个空的帖子列表。有关我的修改,请参见上文。您能否提供示例代码来说明您的解决方案?
  • 我刚刚发布了我们项目中的示例类!

标签: android json gson retrofit


【解决方案1】:

在向 GSON 提供通用的基本响应类几个小时失败后,我最终通过了这条路线并确定了我几天前实施的解决方案(减去状态检查条件)。

GSON 通过在通用 TypeAdapterFactory 中定义反序列化逻辑,提供向所有响应添加 TypeAdapter 的能力。这个实体并不像我希望的那样干净和无知,但它在减少必要的响应模型类数量的同时还维护了一个适配器。

private static class ResponseTypeAdapterFactory implements TypeAdapterFactory {

    private static final String STATUS = "status";
    private static final String SUCCESS = "success";
    private static final String DATA = "data";

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        final TypeAdapter<T> delegateAdapter = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) throws IOException {
                delegateAdapter.write(out, value);
            }

            @Override
            public T read(JsonReader in) throws IOException {
                // Ignore extraneous data and read in only the response data when the response is a success
                JsonElement jsonElement = jsonElementAdapter.read(in);
                if (jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();
                    if (jsonObject.has(STATUS)) {
                        if (jsonObject.get(STATUS).getAsString().equals(SUCCESS)) {
                            if (jsonObject.has(DATA) && jsonObject.get(DATA).isJsonObject()) {
                                jsonElement = jsonObject.get(DATA);
                            }
                        }
                    }
                }
                return delegateAdapter.fromJsonTree(jsonElement);
            }
        }.nullSafe();
    }
}

简而言之,如果响应成功,我会告诉 GSON 抓取“数据”JSON 对象。否则,返回整个响应正文,以便我的自定义 Retrofit 错误处理程序可以使用从服务器返回的“error_title”和“error_message”字段。

非常感谢 @david.mihola 提出的好建议,并最终将我的注意力引回到 TypeAdapterFactory 解决方案上。

【讨论】:

  • 在改造 1.9 中,这就像一个魅力,但在改造 2.0 中,我无法实现这一点。关于如何使这项工作适用于改造 2.0 的任何想法?谢谢\
【解决方案2】:

这并不是您问题的真正答案,但可能是针对同一问题的替代解决方案,即为许多类似的响应提供多个冗余类:

这是我们的AbstractResponse

public abstract class AbstractResponse<T> {

    @SerializedName("success")
    private boolean success;

    // used for error handling
    @SerializedName("error")
    private String errorMessage;

    @SerializedName("code")
    private Integer errorCode;

    // used for normal operation
    @SerializedName("data")
    protected T data;

    @SerializedName("details")
    private DetailsError details;

    @SerializedName("points")
    private Integer points;

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public DetailsError getDetailsError() {
        return details;
    }

    public Integer getPoints() {
        return points;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public Integer getErrorCode() {
        return errorCode;
    }

    public AbstractResponse(T data) {
        this.data = data;
    }

    @Override
    public String toString() {
        return "AbstractResponse{" +
                "success=" + success +
                ", errorMessage='" + errorMessage + '\'' +
                ", errorCode=" + errorCode +
                ", data=" + data +
                '}';
    }
}

还有像这样的类:

public class VotingImageListResponse extends AbstractResponse<List<VotingImage>> {

    public VotingImageListResponse(List<VotingImage> data) {
        super(data);
    }

}

Retrofit 像这样使用它们:

@GET("/api/VotingImage")
public void getVotingImages(@Query("voting_id") Integer id, @Query("app_user_id") Integer userId, @Query("session") String sessionId, Callback<VotingImageListResponse> callback);

仅此而已。

编辑

为了更清楚,这里是VotingImage

public class VotingImage implements Parcelable {

    @SerializedName("voting_image_id")
    private final Integer votingImageId;

    @SerializedName("voting_id")
    private final Integer votingId;

    @SerializedName("image_id")
    private final Integer imageId;

    @SerializedName("url")
    private final Uri uri;

    @SerializedName("url_small")
    private final Uri uriSmall;

    // ...
}

更多具体响应类的例子:

public class ChoiceResponse extends AbstractResponse<Choice> {
    public ChoiceResponse(Choice data) {
        super(data);
    }
}

Choice 的定义如下:

public class Choice {

    @SerializedName("question_list")
    private final PVector<Question> questions;

    @SerializedName("is_evaluation")
    private final Boolean isEvaluation;

   // ...
}

或者:

public class RegisterResponse extends AbstractResponse<RegisterResponseData>{
    public RegisterResponse(RegisterResponseData data) {
        super(data);
    }
}

与:

public class RegisterResponseData {

    @SerializedName("mail")
    private String email;

    @SerializedName("app_user_id")
    private Integer appUserId;

    @SerializedName("name")
    private String name;

    @SerializedName("session")
    private String sessionId;

    // ...
}

如您所见,尽管 JSON 属性始终称为“数据”,但该字段的类型/内容对于每个响应可能会有很大差异。唯一重要的是 Retrofit 知道(以便它可以告诉 Gson)您预期响应的类型。上面的通用类结构只是-我认为-告诉 Retrofit/Gson 将 JSON 解析为什么的简洁方式。上面的示例方法也可以直接写成这样:

@GET("/api/VotingImage")
public void getVotingImages(@Query("voting_id") Integer id, @Query("app_user_id") Integer userId, @Query("session") String sessionId, Callback<AbstractResponse<List<VotingImage> callback);

还有一件事:这没有经过测试,我现在还不能真正测试它,但是这个怎么样:

public abstract class MyAbstractCallback<T> implements Callback<AbstractResponse<T>> {

    @Callback
    public void onSuccess(AbstractResponse<T> response) {
        // if (response was successful) {
            T data = response.getData();
            onRealSuccess(data);
        // }
    }

    public abstract void onRealSuccess(T data);
}

通过这种方式,您还可以从整个通用响应中分解出实际响应数据的“解包”。

【讨论】:

  • 我认为这与我正在寻找的内容很接近,但不幸的是,API 返回了一个嵌套的、可变名称的 JSON 对象 within“数据”,而我没有认为我可以对您的解决方案做出回应。 “数据”键始终是常量,但“数据”的子元素始终是可变的(例如,上述案例中的“消息”或“用户”、“演讲者”,无论它可能是什么)。如果不在 @SerializedName(key) 注释中明确命名其键,我似乎找不到提取该变量的方法。
  • 一个问题:服务器URL/Retrofit方法和响应的类型有直接对应关系吗? e.对于每个请求,您是否事先知道 JSON 中使用的名称和类型?
  • HTTP URL 和数据的子类型没有关系。我知道类型的唯一方法是查看 API 文档以检查密钥。我正在想办法根据给定的Callback&lt;T&gt; 类型将表示数据子密钥的字符串传递给 GSON 反序列化器。
  • 我的意思不一定是“明显”或“不言而喻”的关系。 e. URL“/myapi//”总是会在 JSON 中返回一个“X”。但是:当您调用 API 时,您知道会发生什么吗?
  • 是的,我知道对任何 API 响应的期望。 Callback 类型暗示​​了这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-25
  • 2016-04-29
  • 2018-08-03
  • 2020-09-17
  • 1970-01-01
相关资源
最近更新 更多