Gson: indexed object to list



I have a json object from an external API that has a “list” of objects in the form of:

{
  "width": 10,
  "height": 20
  "pointData": {
    "0": {
      "x": 7,
      "y": 5,
      ...
    },
    "1": {
      "x": 4,
      "y": 20,
      ...
    },
    "2": {
      "x": 1,
      "y": 3,
      ...
    },
    ...
  }
}

My Deserializer and POJOs looks something like this:

class KeyedListDeserializer<T> implements JsonDeserializer<List<T>> {

  @Override
  public List<T> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
    JsonObject jsonObject = json.getAsJsonObject();
    List<T> things = new ArrayList<>();
    int key = 0;
    while (jsonObject.has(String.valueOf(key))) {
      JsonElement element = jsonObject.get(String.valueOf(key));
      T value = context.deserialize(element, new TypeToken<T>(){}.getType());
      things.add(value);
      key += 1;
    }
    return things;
  }
}

class PointDataKeyedList extends KeyedListDeserializer<PointData> {}

class Canvas {
  int width;
  int height;
  @JsonAdapter(PointDataKeyedList.class)
  List<PointData> pointData;
  ...
}

class PointData {
  int x;
  int y;
  ...
}

Since all the keys of the pointData object are just indices I thought I could just deserialize this as a list rather than a map. When running this, Gson successfully gets through deserialization, but when I try to access the point data, I get a:

Exception in thread "main" java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class PointData (com.google.gson.internal.LinkedTreeMap and PointData are in unnamed module of loader 'app')

I’m not sure what’s going wrong here. I’ve read a bit about type erasure, which is something I’ve heard come up a lot when searching this problem, but I don’t really understand it very well.

Answer

It’s pretty easy in Gson.

public final class MapToListTypeAdapterFactory
        implements TypeAdapterFactory {

    private MapToListTypeAdapterFactory() {
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
            return null;
        }
        final Type elementType = typeToken.getType() instanceof ParameterizedType
                ? ((ParameterizedType) typeToken.getType()).getActualTypeArguments()[0]
                : Object.class;
        final TypeAdapter<?> elementTypeAdapter = gson.getAdapter(TypeToken.get(elementType));
        final TypeAdapter<List<Object>> listTypeAdapter = new TypeAdapter<List<Object>>() {
            @Override
            public void write(final JsonWriter out, final List<Object> value) {
                throw new UnsupportedOperationException();
            }

            @Override
            public List<Object> read(final JsonReader in)
                    throws IOException {
                in.beginObject();
                final ArrayList<Object> list = new ArrayList<>();
                while ( in.hasNext() ) {
                    final int index = Integer.parseInt(in.nextName());
                    final Object element = elementTypeAdapter.read(in);
                    ensureSize(list, index + 1);
                    list.set(index, element);
                }
                in.endObject();
                return list;
            }
        };
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) listTypeAdapter
                .nullSafe();
        return typeAdapter;
    }

    // https://stackoverflow.com/questions/7688151/java-arraylist-ensurecapacity-not-working/7688171#7688171
    private static void ensureSize(final ArrayList<?> list, final int size) {
        list.ensureCapacity(size);
        while ( list.size() < size ) {
            list.add(null);
        }
    }

}

The type adapter factory above does the following things:

  • checks if the target type is List (it does not really work with linked lists but it’s fine for simplification);
  • extracts the type parameter of the target list, hence its elements type and resolves a corresponding type adapter;
  • substitutes the original type adapter with a map-reading one that reads map keys assuming them as the new list indices (and enlarges the result list if necessary) and reads every map value using the original element type adapter. Note that the assumption that the list may have sparse indices is vulnerable and I only put it for demo purposes (say, the input JSON declares a single-element map with the only index “999999” that enlarges the list dramatically), so you can ignore the index value using the add(element) method only.

Simply annotate the pointData field in the Canvas class with @JsonAdapter(MapToListTypeAdapterFactory.class) and it will work.



Source: stackoverflow