Search code examples
gsondeserialization

Optimal approach for utilizing Gson when only concerned with JSON keys?


Imagine having a JSON structure containing a list of purchased products by customers:

{
  "product_key_1": {
    "name": "product_name_1",
    "price": 10.99
  },
  "product_key_2": {
    "name": "product_name_2",
    "price": 24.99
  },
  "product_key_3": {
    "name": "product_name_3",
    "price": 7.49
  }
}

However, in this scenario, the client is solely interested in extracting the keys representing the products and isn't concerned with the specific details like product names or prices.

What would be the recommended approach for deserializing this JSON string using Gson, with the primary goal of obtaining the keys?

Currently, I am employing the following method:

Map<String, Object> products = gson.fromJson(rawJsonString, Object.class);
Set<String> productKeys = products.keySet(); 

This works but I am wondering if this is the correct way."

EDIT: seems like another way to do this is to convert json to a Map<String, JsonElement>

Map<String, JsonElement> products = gson.fromJson(rawJsonString, new TypeToken<Map<String, JsonElement>>() {}.getType());

Solution

  • If you are only interested in the map keys, deserializing the map values as Object or JsonElement is rather wasteful because Gson will still construct the corresponding objects, even if you don't use them. What you could do instead is create an empty class and specify that as value type (assuming that the map values are always of type JSON object). Because Gson by default ignores unknown fields, it will efficiently skip all the fields in the values of the JSON object members.

    The code could then look like this:

    class IgnoredValue {
    }
    
    Map<String, IgnoredValue> map = gson.fromJson(rawJsonString, new TypeToken<Map<String, IgnoredValue>>() {});
    System.out.println(map.keySet());
    

    Additionally you could also register a custom TypeAdapter for the value type, which always skips the JSON value and returns null. That might be even more efficient and would also work if the map values are something other than JSON objects (for example JSON arrays, numbers or strings):

    Gson gson = new GsonBuilder()
        .registerTypeAdapter(IgnoredValue.class, new TypeAdapter<IgnoredValue>() {
            @Override
            public IgnoredValue read(JsonReader in) throws IOException {
                in.skipValue();
                return null;
            }
            @Override
            public void write(JsonWriter out, IgnoredValue value) throws IOException {
                throw new UnsupportedOperationException();
            }
        })
        .create();
    Map<String, IgnoredValue> map = gson.fromJson(rawJsonString, new TypeToken<Map<String, IgnoredValue>>() {});
    

    Alternatively, if this JSON data is at the top level (and not nested deeply inside a larger JSON document), then you could also directly use JsonReader if you want:

    List<String> keys = new ArrayList<>();
    try (JsonReader jsonReader = new JsonReader(new StringReader(rawJsonString))) {
        jsonReader.beginObject();
    
        // Handle all JSON object members
        while (jsonReader.hasNext()) {
            keys.add(jsonReader.nextName());
            jsonReader.skipValue();
        }
    
        jsonReader.endObject();
    }
    

    If the JSON data is deeply nested and you have an enclosing deserialized class, then you could add a List<String> products field, create a TypeAdapter which contains the above JsonReader code in its read method, and add a @JsonAdapter annotation which refers to your type adapter on that field.