Как обрабатывать параметры, которые могут быть ARRAY или OBJECT в Retrofit на Android?

У меня возникла проблема, когда обработчик API, который я обрабатываю, возвращает OBJECT для ARRAY размера 1.

Например, иногда API отвечает:

{
    "monument": [
        {
            "key": 4152,
            "name": "MTS - Corporate Head Office",
            "categories": {},
            "address": {}
        },
        {
            "key": 4151,
            "name": "Canadian Transportation Agency",
            "categories": {},
            "address": {}
        },
        {
            "key": 4153,
            "name": "Bank of Montreal Building",
            "categories": {},
            "address": {}
        }
    ],
}

Однако, если массив monument имеет только 1 элемент, он становится ОБЪЕКТОМ (обратите внимание на отсутствие скобок []) так:

{
    "monument": {
        "key": 4152,
        "name": "MTS - Corporate Head Office",
        "categories": {},
        "address": {}
    }
}

Если я определяю свои модели следующим образом, я получаю сообщение об ошибке, когда возвращается только один элемент:

public class Locations {
    public List<Monument> monument;
}

Если возвращается только один элемент, я получаю следующую ошибку:

Expected BEGIN_OBJECT but was BEGIN_ARRAY ...

И если я определяю свою модель следующим образом:

public class Locations {
    public Monument monument;
}

и API возвращает ARRAY. Я получаю противоположную ошибку.

Expected BEGIN_ARRAY  but was BEGIN_OBJECT ...

Я не могу определить несколько элементов с тем же именем в моей модели. Как я могу справиться с этим случаем?

Примечание. Я не могу вносить изменения в API.

Ответ 1

В качестве дополнения к моему предыдущему ответу, здесь решение с использованием TypeAdapter.

public class LocationsTypeAdapter extends TypeAdapter<Locations> {

    private Gson gson = new Gson();

    @Override
    public void write(JsonWriter jsonWriter, Locations locations) throws IOException {
        gson.toJson(locations, Locations.class, jsonWriter);
    }

    @Override
    public Locations read(JsonReader jsonReader) throws IOException {
        Locations locations;

        jsonReader.beginObject();
        jsonReader.nextName();       

        if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
            locations = new Locations((Monument[]) gson.fromJson(jsonReader, Monument[].class));
        } else if(jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
            locations = new Locations((Monument) gson.fromJson(jsonReader, Monument.class));
        } else {
            throw new JsonParseException("Unexpected token " + jsonReader.peek());
        }

        jsonReader.endObject();
        return locations;
    }
}

Ответ 2

Трюк записывает ваш собственный десериализатор Gson для вашего класса Locations. Это проверит, является ли элемент памятника объектом или массивом. Например:

public class LocationsDeserializer implements JsonDeserializer<Locations> {

    @Override
    public Locations deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

        JsonElement monumentElement = json.getAsJsonObject().get("monument");
        if (monumentElement.isJsonArray()) {
            return new Locations((Monument[]) context.deserialize(monumentElement.getAsJsonArray(), Monument[].class));
        } else if (monumentElement.isJsonObject()) {
            return new Locations((Monument) context.deserialize(monumentElement.getAsJsonObject(), Monument.class));
        } else {
            throw new JsonParseException("Unsupported type of monument element");
        }
    }
}

Для удобства добавьте конструктор vararg в класс Locations:

public class Locations {
    public List<Monument> monuments;

    public Locations(Monument ... ms) {
        monuments = Arrays.asList(ms);
    }
}

Ваш класс Памяти остается прежним. Что-то вроде:

public class Monument {
    public int key;
    public String name;
    // public Categories categories;
    // public Address address;
}

Наконец, создайте свой собственный объект Gson и передайте его в модификацию RestAdapter.

Gson gson = new GsonBuilder().registerTypeAdapter(Locations.class, new LocationsDeserializer()).create();

RestAdapter restAdapter = new RestAdapter.Builder()
            .setEndpoint(baseUrl)
            .setConverter(new GsonConverter(gson))
            .build();

Ответ 3

Вы можете зарегистрировать TypeAdapter с помощью Gson, который условно обрабатывает это поведение.

Вы сначала назовёте peekToken(). Если он был BEGIN_ARRAY, тогда просто десериализуем как List<Foo>. Если он BEGIN_OBJECT, то десериализуйтесь как Foo и заверните в Collections.singletonList.

Теперь у вас всегда есть список, будь то один элемент или много.