Адаптация Повторная настройка ответов с использованием GSON

Я хотел бы агностически получить дочерний элемент известного объекта JSON с каждым успешным ответом, который я получаю от конкретного API.

Каждый ответ сервера возвращает следующий формат 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" }
        ]
    }
}

Ответы об ошибках содержат один и тот же общий формат, при этом пустой объект "data" и связанные с ошибкой объекты JSON, содержащие полезные значения. В случае ошибки я хотел бы извлечь связанные с ошибкой объекты JSON.

С приведенным выше ответом у меня есть класс MessageResponse, который содержит свойства статуса, errorTitle и errorMessage String, а также объект MessageData. Затем объект MessageData содержит список сообщений - List<Message> messages. Мой метод GET для получения сообщений в этом случае выглядит следующим образом (сжатый для простоты):

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

Эта конструкция требует трех классов для каждого типа ответа, если я должен придерживаться стандартного POJO-сопоставления, которое Serializer GSON предоставляет из коробки. Моя конечная цель - сократить количество классов, необходимых, читая только то, что мне нужно, от успешного ответа сервера и игнорируя остальные. Я хотел бы, чтобы все мои успехи, типы данных обратного вызова в этом API были как можно ближе к содержанию "данных".

Другими словами, я хотел бы агностически вернуть дочерний элемент "данных". В приведенном выше случае это массив, называемый "сообщениями", но в некоторых других ответах он может быть, например, "пользовательским" объектом. Я знаю, что это можно сделать, зарегистрировав отдельный TypeAdapter для каждого типа ответа, но я хотел бы достичь своей конечной цели, используя одно общее решение.

ОБНОВЛЕНИЕ: реализация предложения Дэвида снизу

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 из родителя "data" и десериализовать этот дочерний элемент в класс модели, на который ссылается общий тип данных. По существу, dataJsonObject.getChild().

Ответ 1

После нескольких часов неудачной подачи общих классов базового ответа на GSON я закончил передачу этого маршрута и установил решение, которое я реализовал несколько дней назад (за вычетом условий проверки состояния).

GSON предоставляет возможность добавлять TypeAdapter ко всем ответам, определяя логику десериализации в общем TypeAdapterFactory. Этот объект не такой чистый и невежественный, как я надеялся, что он будет, но он выполняет работу по сокращению числа необходимых классов модели ответов, одновременно поддерживая один адаптер.

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.

Ответ 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);
}

Таким образом, вы также можете отменить "распаковку" фактических данных ответа из всего общего ответа.