How to use GSON streaming to parse JSON with multiple top-level objects?

218 Views Asked by At

I've been looking for example Gson streaming API code, and I see a lot of this style:

       reader.beginObject();
       while (reader.hasNext()) {

          switch(reader.peek()) {
             case NAME:
                ...
             case BEGIN_ARRAY:
                ...
             case END_ARRAY:
                ...

                }

       reader.endObject();
       reader.close();

That code works great when the JSON has only one "top-level" object. The hasNext() loop terminates at the end of that first object. What I need is a loop around that to process ALL the objects. Here's an abbreviated example of my JSON:

{
  "meta": {
    "disclaimer": "Loren Ipsum",
    "terms": "https://myURL/terms/",
    "license": "https://myURL/license/",
    "last_updated": "2023-03-10",
    "totals": {
      "skip": 0,
      "limit": 12000,
      "total": 362818
    }
  },
  "results": [
    {"result": "value1"},
    {"result": "value2"},
    {"result": "value3"},
  ]
}

This code processes the "meta" object just fine, and never get to the "results" array.

Searched high and low.

1

There are 1 best solutions below

2
Marcono1234 On

Normally you never have to handle NAME and END_ARRAY when using peek():

  • JSON specifies that objects consist of pairs of member names and values. So you only need a call hasNext() in a loop, and while the result is true call nextName(), followed by a value reading method such as nextInt() (possibly after checking the type with peek() first).
  • If the loop condition is hasNext() then you won't encounter an END_ARRAY inside the loop.

The general structure for reading JSON arrays and objects from a JsonReader is:

jsonReader.begin###();
while (jsonReader.hasNext()) {
    ... // read values
}
jsonReader.end###();

And you can arbitrarily nest this. Here is an example for this, based on your JSON data. The main point of this example is to demonstrate the nesting, the code is not very useful and also quite verbose:

jsonReader.beginObject();
while (jsonReader.hasNext()) {
    switch (jsonReader.nextName()) {
        case "meta": {
            jsonReader.beginObject();
            while (jsonReader.hasNext()) {
                String name = jsonReader.nextName();
                switch (jsonReader.peek()) {
                    case STRING:
                        System.out.println(name + ": " + jsonReader.nextString());
                        break;
                    default:
                        jsonReader.skipValue();
                        System.out.println(name + ": <skipped>");
                        break;
                }
            }
            jsonReader.endObject();
            break;
        }
        case "results": {
            jsonReader.beginArray();
            while (jsonReader.hasNext()) {
                jsonReader.beginObject();
                while (jsonReader.hasNext()) {
                    System.out.println(jsonReader.nextName() + ": " + jsonReader.nextString());
                }
                jsonReader.endObject();
            }
            jsonReader.endArray();
            break;
        }
        default:
            throw new IllegalArgumentException("Unsupported name");
    }
}
jsonReader.endObject();

But is it really necessary in your use case to directly read from a JsonReader? If not, then it would be probably less error-prone to just create model classes matching your JSON data (e.g. class Meta { String disclaimer; URL terms; ... }) and use one of the Gson.fromJson methods with a Reader as parameter. Internally that will also parse JSON in a streaming way, so the only overhead is that afterwards the complete deserialized object exists in memory. But maybe that is exactly what you need.