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.
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:
List
(it does not really work with linked lists but it's fine for simplification);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.