burakk
burakk

Reputation: 1291

Retrofit2 - GSON - Expected BEGIN_ARRAY but was STRING or Expected BEGIN_OBJECT but was BEGIN_ARRAY

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

Answers (2)

lipawisc
lipawisc

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

Alex
Alex

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:

  • Defines what should be done when reading the json -> read() method.
  • Defines what should be done when writing the json -> 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

  • We first check whether we receive a String in json by using reader.peek() == JsonToken.STRING in the read() method. If so, we create the list ourselves.
  • If we get an array, we parse it as it used to be.

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

Related Questions