3

I am using Gson to parse a JSON response from a certain API. Everything was working ok, but now seems like one of the fields of the response can come in an array form or in a single element form, so that I am getting com.google.gson.JsonSyntaxException: java.lang.IllegalStateException:

Here you can see the fragment of the two JSON versions causing problems:

VERSION 1

{
    "Notes": {
        "Note": [
            {
                "key": "disruption-message",
                "section": "high",
                "priority": "1",
                "message": "The battery consumption raised suddenly."
            },
            {
                "key": "disruption-message",
                "section": "low",
                "priority": "2",
                "message": "The power on the converter might be too high."
            }
        ]
    }
}

VERSION 2

{
    "Notes": {
        "Note": {
            "key": "medium",
            "section": "low",
            "priority": "1",
            "message": "Life time for the battery will expire soon"
        }
    }
}

To parse the VERSION 1, I am using the following class:

public class Notes implements Serializable {

    @SerializedName("Note")
    @Expose
    private List<Note> note = null;

    public List<Note> getNote() {
        return note;
    }

    public void setNote(List<Note> note) {
        this.note = note;
    }

}

This works with the VERSION 1 but when it finds a part of the JSON response matching the VERSION 2, of course it gives:

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT 

How can I make it deserialize Notes, whatever format they have?

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
codeKiller
  • 5,493
  • 17
  • 60
  • 115
  • try this link http://stackoverflow.com/questions/21767485/gson-deserialization-to-specific-object-type-based-on-field-value – Meenal May 09 '17 at 11:39

2 Answers2

5

This question belongs to one of the most famous Gson-related question groups, I guess, and improperly designed JSON responses hurt. You can find the exact solution here: Make GSON accept single objects where it expects arrays . Once you have that type adapter factory, you can annotate your mappings like these:

final class ResponseV1 {

    @SerializedName("Notes")
    final NotesWrapperV1 notes = null;

}
final class NotesWrapperV1 {

    @SerializedName("Note")
    @JsonAdapter(AlwaysListTypeAdapterFactory.class)
    final List<Note> notes = null;

}
final class Note {

    final String key = null;
    final String section = null;
    final String priority = null;
    final String message = null;

}

IMO, you can proceed even further and just remove the inner wrapper class.

final class ResponseV2 {

    @SerializedName("Notes")
    @JsonAdapter(NestedNotesTypeAdapterFactory.class)
    final List<Note> notes = null;

}

Where NestedNotesTypeAdapterFactory is implemented like this:

final class NestedNotesTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeToken<List<Note>> noteListTypeToken = new TypeToken<List<Note>>() {
    };

    private NestedNotesTypeAdapterFactory() {
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // Just add the factory method to AlwaysListTypeAdapterFactory and let it just return the class singleton (the factory is stateless, so it can be constructed once)
        final TypeAdapter<List<Note>> noteListTypeAdapter = getAlwaysListTypeAdapterFactory().create(gson, noteListTypeToken);
        final TypeAdapter<List<Note>> nestedNotesTypeAdapter = new NestedNotesTypeAdapter(noteListTypeAdapter);
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) nestedNotesTypeAdapter;
        return typeAdapter;
    }

    private static final class NestedNotesTypeAdapter
            extends TypeAdapter<List<Note>> {

        private final TypeAdapter<List<Note>> noteListTypeAdapter;

        private NestedNotesTypeAdapter(final TypeAdapter<List<Note>> noteListTypeAdapter) {
            this.noteListTypeAdapter = noteListTypeAdapter;
        }

        @Override
        public void write(final JsonWriter out, final List<Note> value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public List<Note> read(final JsonReader in)
                throws IOException {
            // "Unwrap" the Note property here
            in.beginObject();
            List<Note> notes = null;
            while ( in.hasNext() ) {
                final String name = in.nextName();
                switch ( name ) {
                case "Note":
                    // If we've reached the Note property -- just read the list
                    notes = noteListTypeAdapter.read(in);
                    break;
                default:
                    throw new MalformedJsonException("Unrecognized " + name + " at " + in);
                }
            }
            in.endObject();
            return notes;
        }

    }
}

Test cases for both implementations:

for ( final String resource : ImmutableList.of("version-1.json", "version-2.json") ) {
    System.out.println(resource);
    try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43868120.class, resource) ) {
        final ResponseV1 response = gson.fromJson(jsonReader, ResponseV1.class);
        for ( final Note note : response.notes.notes ) {
            System.out.println(note.message);
        }
    }
}
for ( final String resource : ImmutableList.of("version-1.json", "version-2.json") ) {
    System.out.println(resource);
    try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43868120.class, resource) ) {
        final ResponseV2 response = gson.fromJson(jsonReader, ResponseV2.class);
        for ( final Note note : response.notes ) {
            System.out.println(note.message);
        }
    }
}

Both produce the following:

version-1.json
The battery consumption raised suddenly.
The power on the converter might be too high.
version-2.json
Life time for the battery will expire soon

Community
  • 1
  • 1
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • thanks man! looks a bit complicated solution but definitely worth to give it a try, I will see what can I get out of all this :) Thanks again! – codeKiller May 10 '17 at 06:40
  • so according to your answer, should I use just ResponseV2, with the name of `Notes` together with `Note`class? I am a bit confused sorry – codeKiller May 12 '17 at 11:19
  • @eddie You may. `V2` just stands for the 2nd approach where no intermediate wrapper is required unlike V1. `Note` is shared between two versions. You choose either V1 or V2 like you want. – Lyubomyr Shaydariv May 12 '17 at 11:22
  • is this supposed to work if, ON THE SAME JSON response, I have a `Notes`field with a `Note`in array version and, further down, another `Notes`with `Note`in single object version?? – codeKiller May 12 '17 at 12:15
  • @eddie Mate, the answer shows two examples (my V1 and V2): both of them work on all your two JSON documents (your V1 and V2) giving the same result. I didn't notice that I was using clashing terminology for word "version". Just try it. – Lyubomyr Shaydariv May 12 '17 at 12:34
0

The problem you are encountering is GSON is expecting an array but the JSON is a serialisation of and object.

Basically, GSON expects '[' but found '{'

So you should modify your code for version2 to deserialize the JSON into a single Note object.

Also, you should look into both of the JSON versions.

According to GSON your VERSION 2 JSON is slightly invalid You have a list of Note objects which must be denoted as
"Note"[ notes... ]

which is precisely what it is in version 1.

I suggest changing your version 2 JSON to something similar

"Notes"{ "Note":[ whatever note objects you have go in here. ] }

If altering the JSON is not in your powers, then consider having another class for cases similar to this.

You should create new Class with @SerializedNames and a Note object then use it to deserialize the json.

Nikhil R
  • 19
  • 1
  • 5
  • thanks for your answer, and you are right, altering JSON response is NOT UNDER MY CONTROL....so I have to work with what I have I am afraid. – codeKiller May 09 '17 at 11:35
  • hey Nikhil, I am not sure I follow you.....creating another class is quite simple, however, you have to think that my JSON response is much longer than the piece I posted so, more `Notes` fields might come in the same JSON, and some of them in VERSION 1 and some in VERSION 2....then, how to know which class to use each time for `{Notes}` or `[Notes]` – codeKiller May 09 '17 at 11:47
  • You can try getting the json as a json object and then get its member "Note" – Nikhil R May 09 '17 at 11:50
  • check its type whether its an JSONArray or an object and from there its should be a cake walk – Nikhil R May 09 '17 at 11:51