Reputation: 8461
I'm having an issue where the API I'm parsing returns an OBJECT for an ARRAY of size 1.
For example, sometimes the API will respond with:
{
"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": {}
}
],
}
However, if the monument
array has only 1 item it becomes an OBJECT (note the lack of []
brackets) like so:
{
"monument": {
"key": 4152,
"name": "MTS - Corporate Head Office",
"categories": {},
"address": {}
}
}
If I define my models like this, I will get an error when only a single item is returned:
public class Locations {
public List<Monument> monument;
}
If only a single item is returned I get the following error:
Expected BEGIN_OBJECT but was BEGIN_ARRAY ...
And if I define my model like so:
public class Locations {
public Monument monument;
}
and the API returns an ARRAY I get the opposite error
Expected BEGIN_ARRAY but was BEGIN_OBJECT ...
I cannot define multiple items with the same name in my model. How can I handle this case?
Note: I cannot make changes to the API.
Upvotes: 19
Views: 8328
Reputation: 403
You can register a TypeAdapter
with Gson which conditionally handles this behavior.
You'd call peekToken()
first. If it was BEGIN_ARRAY
then just deserialize as List<Foo>
. If it's BEGIN_OBJECT
then deserialize as Foo
and wrap in Collections.singletonList
.
Now you always have a list whether it's a single item or many.
Upvotes: 0
Reputation: 1466
As a complement to my previous answer, here's a solution using a 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;
}
}
Upvotes: 12
Reputation: 1466
The trick is to write your own Gson deserializer for your Locations
class. This would check whether the monument element is an object or an array. Like so:
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");
}
}
}
For the convenience, add a vararg constructor to your Locations
class:
public class Locations {
public List<Monument> monuments;
public Locations(Monument ... ms) {
monuments = Arrays.asList(ms);
}
}
Your Monument class stays the same. Something like:
public class Monument {
public int key;
public String name;
// public Categories categories;
// public Address address;
}
Finally, create your own Gson
object and pass it to the retrofit RestAdapter
.
Gson gson = new GsonBuilder().registerTypeAdapter(Locations.class, new LocationsDeserializer()).create();
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(baseUrl)
.setConverter(new GsonConverter(gson))
.build();
Upvotes: 14