Десериализовать JSON в POJO с помощью Retrofit и Gson

Я работаю с API, который отвечает следующим образом для одного пользовательского ресурса:

{
  "data": {
    "id": 11,
    "first_name": "First",
    "last_name": "Last",
    "books": {
      "data": [
        {
          "id": 13,
          "name": "Halo"
        }
      ]
    },
    "games": {
      "data": [
        {
          "id": 1,
          "name": "Halo"
        }
      ]
    }
  }
}

или как показано ниже для нескольких пользовательских ресурсов:

{
  "data": [
    {
      "id": 11,
      "first_name": "First",
      "last_name": "Last",
      "books": {
        "data": [
          {
            "id": 13,
            "name": "Halo"
          }
        ]
      },
      "games": {
        "data": [
          {
            "id": 1,
            "name": "Halo"
          }
        ]
      }
    },
  ],
  "meta": {
    "pagination": {
      "total": 11,
      "count": 10,
      "per_page": 10,
      "current_page": 1,
      "total_pages": 2,
      "links": {
        "next": "http://api.###.com/users?page=2"
      }
    }
  }
}

Основные моменты, на которые следует обратить внимание:

  1. все ресурсы вложены в ключ data, одиночный как объект или множественный как массив объектов. Сюда входят вложенные ресурсы, такие как книги и игры в приведенном выше примере.
  2. Мне нужно иметь возможность получить значения ключа meta для моих процедур разбивки на страницы.

Пользовательская модель

public class User extends BaseModel {

    public Integer id;
    public String firstName;
    public String lastName;

    public List<Book> books; // These will not receive the deserialized 
    public List<Game> games; // JSON due to the parent data key

}

Пользовательский десериализатор JSON

public class ItemTypeAdapterFactory implements TypeAdapterFactory {

    public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {

        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {

            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            public T read(JsonReader in) throws IOException {

                JsonElement jsonElement = elementAdapter.read(in);
                if (jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();
                    // If the data key exists and is an object or array, unwrap it and return its contents
                    if (jsonObject.has("data") && (jsonObject.get("data").isJsonObject() || jsonObject.get("data").isJsonArray())) {
                        jsonElement = jsonObject.get("data");
                    }
                }

                return delegate.fromJsonTree(jsonElement);
            }
        }.nullSafe();
    }
}

Все работает нормально, но я не могу понять, как получить доступ к ключу meta для разбиения на страницы.

В идеале я бы попросил Gson десериализовать ответ на следующий POJO:

public class ApiResponse {
    public Object data;
    public Meta meta
}

и я мог бы просто привести поле response к правильному типу в обратном вызове ответа, как показано ниже:

Map<String, String> params = new HashMap<String, String>();
params.put("include", "books,games");
ApiClient.getClient().authenticatedUser(params, new ApiClientCallback<ApiResponse>() {
    @Override
    public void failure(RestError restError) {
        Log.d("TAG", restError.message);
    }

    @Override
    public void success(ApiResponse response, Response rawResponse) {
        User user = (User) response.data; // Cast data field to User type
        Log.d("TAG", user.firstName);
        Log.d("TAG", "Total pages" + response.meta.pagination.total.toString()); // Still have access to meta key data
    }
});

Однако поле data объекта ApiResponse равно null.

Моя Java очень ржавая, и я понятия не имею, возможно ли это вообще, и я не понимаю, как это сделать правильно, любая помощь будет очень признательна.


person David Hancock    schedule 12.08.2015    source источник


Ответы (1)


Мне нужно было то же самое, и мне удалось заставить его работать, добавив оператор if в ваш собственный сериализатор:

…
    // If the meta key exists, consider the element to be root and don't unwrap it
    if (!jsonObject.has("meta")) {
        // If the data key exists and is an object or array, unwrap it and return its contents
        if (jsonObject.has("data") && (jsonObject.get("data").isJsonObject() || jsonObject.get("data").isJsonArray())) {
            jsonElement = jsonObject.get("data");
        }
    }
…

Причина, по которой поле data вашего ApiResponse было null, заключается в том, что ваш исходный десериализатор обрабатывал весь ответ и заставлял вас «ослаблять» элементы data и meta корневого объекта.

Я также параметризовал класс ApiResponse:

public class ApiResponse<T> {
    public Meta meta;
    public T data;
}

Таким образом, десериализация по-прежнему работает без создания множества разных классов Response, при этом приведение больше не требуется, и вы можете указать тип поля data ApiResponse по мере необходимости (например, ApiResponse<User> для одного пользовательского ресурса, ApiResponse<List<User>> для нескольких пользовательских ресурсов и т. д.).

person Slavko    schedule 19.09.2015