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.
Advertisement
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 theadd(element)
method only.
Simply annotate the pointData
field in the Canvas
class with @JsonAdapter(MapToListTypeAdapterFactory.class)
and it will work.