Reputation: 1291
I am getting the following exception on my Retrofit2 call:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 22311 path $[1].programme[3].credits.presenter
I probably know why it is being thrown. Some "credits" objects have more than one "presenters", and they are given as JSON Arrays ({"presenter":["Barbara Schledduytkew","Hubert Muckhutgoldwes"]}), however, some others have only a single "presenter", and it is given as a JSON Object ({"presenter":"Rosalynda Demstogtrojkt"})
I need to find a way to get the presenter items parsed in both cases. I guess that I have to write my own custom TypeAdapter, but I am not sure and need help. Thanks in advance.
Retrofit Instance:
retrofit = new Retrofit.Builder()
.baseUrl(Constants.URL_HOST)
.client(okHttpClient.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
Credits Class:
public class Credits {
@SerializedName("director")
@Expose
private List<String> director;
@SerializedName("actor")
@Expose
private List<String> actor = null;
@SerializedName("presenter")
@Expose
private List<String> presenter;
@SerializedName("commentator")
@Expose
private String commentator;
public List<String> getDirector() {
return director;
}
public void setDirector(List<String> director) {
this.director = director;
}
public List<String> getActor() {
return actor;
}
public void setActor(List<String> actor) {
this.actor = actor;
}
public List<String> getPresenter() {
return presenter;
}
public void setPresenter(List<String> presenter) {
this.presenter = presenter;
}
public String getCommentator() {
return commentator;
}
public void setCommentator(String commentator) {
this.commentator = commentator;
}
}
Upvotes: 0
Views: 670
Reputation: 91
You can accomplish this through a custom JsonDeserializer that checks whether the presenter
key is a String or an Array:
public final class CreditsJsonDeserializer implements JsonDeserializer<Credits> {
private CreditsJsonDeserializer() {}
private static final CreditsJsonDeserializer INSTANCE = new CreditsJsonDeserializer();
public static CreditsJsonDeserializer instance() {
return INSTANCE;
}
@Override
public Credits deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json.isJsonObject()) {
// Set up an object to return
Credits newCredits = new Credits();
// Loop through the keys in our JSON object
for (Map.Entry<String, JsonElement> kvp : json.getAsJsonObject().entrySet()) {
// Get key
String key = kvp.getKey();
// Get value
JsonElement value = kvp.getValue();
// If we have a null value, just go to the next key
if (value.isJsonNull()) continue;
// Check our key to see which field we need to deserialize
switch (key) {
// Deserialize our List of Directors
case "director":
newCredits.setDirector(context.deserialize(value, new TypeToken<ArrayList<String>>(){}.getType()));
break;
// Deserialize our List of Actors
case "actor":
newCredits.setActor(context.deserialize(value, new TypeToken<ArrayList<String>>(){}.getType()));
break;
// Deserialize our Presenter name or List of Presenters
case "presenter":
// Check if it's a singular name
if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
ArrayList<String> presenters = new ArrayList<>();
presenters.add(value.getAsString());
newCredits.setPresenter(presenters);
}
// Else, it's an Array of names
else {
newCredits.setPresenter(context.deserialize(value, new TypeToken<ArrayList<String>>(){}.getType()));
}
break;
// Deserialize our Commentator name
case "commentator":
newCredits.setCommentator(value.getAsString());
break;
default:
break;
}
}
return newCredits;
}
else {
return null;
}
}
}
Then, change your Retrofit instance code to use the new Deserializer:
Gson gson = new GsonBuilder()
.registerTypeAdapter(Credits.class, CreditsJsonDeserializer.instance())
.create();
retrofit = new Retrofit.Builder()
.baseUrl(Constants.URL_HOST)
.client(okHttpClient.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
Upvotes: 1
Reputation: 1730
You are right on why this error happens. It tries to parse an Array but receives a String. As for your data, it is better to stick with the same model. Which means: Making the backend also returning an array even when it contains only one presenter.
Nevertheless, yes: we can fix this with a custom Type Adapter. This custom type adapter will simply check in the read-part whether it gets a String or an Array. And if it gets a String, we rewrite it to the List variant.
The type adapter
A type adapter does two things:
read()
method.write()
method.The latter one is not used in this case, as we only try to read the data. So we just write the array for the write()
method. Kinda similar to the original type adapter implementation of Collections.
Reading the data
reader.peek() == JsonToken.STRING
in the read()
method. If so, we create the list ourselves. Result The resulting type adapter will look like this:
public class ListFromStringTypeAdapter extends TypeAdapter<List<String>> {
public List<String> read(JsonReader reader) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return null;
}
if (reader.peek() == JsonToken.STRING) {
// ** This is the part where we fix the issue **
// If we receive a String, get this and put it in a list.
// Result will be that item in a list.
List<String> list = new ArrayList<>();
list.add(reader.nextString());
return list;
} else {
// Else we expect to receive the array.
// Based on original collection implementation:
// https://github.com/google/gson/blob/0636635cbffa08157bdbd558b1212e4d806474eb/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java
List<String> list = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
String value = reader.nextString();
list.add(value);
}
reader.endArray();
return list;
}
}
public void write(JsonWriter writer, List<String> list) throws IOException {
// Simply writing the array, we don't need to modify anything here.
// Based on original collection type adapter implementation:
// https://github.com/google/gson/blob/0636635cbffa08157bdbd558b1212e4d806474eb/gson/src/main/java/com/google/gson/internal/bind/CollectionTypeAdapterFactory.java
if (list == null) {
writer.nullValue();
return;
}
writer.beginArray();
for (String string : list) {
writer.value(string);
}
writer.endArray();
}
}
Registering the type adapter As last, we need to tell Gson to use this type adapter for the List class, so modify where you create the Gson object:
// Get the type token for gson
Type collectionStringType = new TypeToken<List<String>>() {}.getType();
// Create gson object
Gson gson = new GsonBuilder()
.registerTypeAdapter(collectionStringType, new ListFromStringTypeAdapter())
.create();
Upvotes: 1