【问题标题】:Gson: indexed object to listGson:要列出的索引对象
【发布时间】:2021-06-12 05:41:25
【问题描述】:

我有一个来自外部 API 的 json 对象,该对象具有以下形式的对象“列表”:

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

我的反序列化器和 POJO 看起来像这样:

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;
  ...
}

由于pointData 对象的所有键都只是索引,我想我可以将其反序列化为列表而不是映射。运行时,Gson 成功通过反序列化,但是当我尝试访问点数据时,我得到:

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')

我不确定这里出了什么问题。我读过一些关于类型擦除的内容,这是我在搜索这个问题时经常听到的,但我并不是很了解。

【问题讨论】:

  • "pointData": {} 是数组的无效表示;只需替换为[] 并解析它。
  • 你在哪里声明KeyedListKeyedListDeserializer之间的关系? (什么是KeyedList?)
  • @njzk2 哎呀,我在尝试将我的实际代码转换为问题时打错了字。 KeyedList 应该是 KeyedListDeserializer。我称它为KeyedList,因为它在技术上是一张地图,但键只是索引,所以它基本上是一个列表。我编辑了问题中的代码
  • @MartinZeitler json 来自外部 API。我不能修改它
  • 为什么不能修改字符串?您可能已经错过了关于“输入清理”的章节......这将使解析/映射它变得更加容易,例如。直接到ArrayList&lt;PointData&gt;,不用再乱来了。

标签: java json gson json-deserialization


【解决方案1】:

我担心你的 POJO 课程需要像这样

class Canvas {
  int width;
  int height;
  Map<String, PointData> pointData;
  ...
}

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

你可以摆脱 PointDataKeyedList 和 KeyedListDeserializer

public static void main(String[] args) {
        String json = "{\n" +
                "  \"width\": 10,\n" +
                "  \"height\": 20,\n" +
                "  \"pointData\": {\n" +
                "    \"0\": {\n" +
                "      \"x\": 7,\n" +
                "      \"y\": 5\n" +

                "    },\n" +
                "    \"1\": {\n" +
                "      \"x\": 4,\n" +
                "      \"y\": 20\n" +
                "    },\n" +
                "    \"2\": {\n" +
                "      \"x\": 1,\n" +
                "      \"y\": 3\n" +
                "    }\n" +
                "  }\n" +
                "}";

        System.out.println(json);
        Gson gson = new Gson();
        Canvas canvas = gson.fromJson(json, Canvas.class);
        System.out.println(canvas);
}

【讨论】:

  • 我知道我可以做到这一点,但由于数据只是一个以索引为键的映射,我想我可以在反序列化时将其转换为实际列表。
【解决方案2】:

在 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);
        }
    }

}

上面的类型适配器工厂做了以下事情:

  • 检查目标类型是否为List(它实际上不适用于链表,但可以简化);
  • 提取目标列表的类型参数,从而提取其元素类型并解析相应的类型适配器;
  • 用映射读取适配器替换原始类型适配器,读取映射键并将它们假定为新列表索引(并在必要时扩大结果列表)并使用原始元素类型适配器读取每个映射值。请注意,列表可能具有稀疏索引的假设很容易受到攻击,我仅将其用于演示目的(例如,输入 JSON 声明了一个具有唯一索引“999999”的单元素映射,这极大地扩大了列表),所以你可以仅使用add(element) 方法忽略index 值。

只需用@JsonAdapter(MapToListTypeAdapterFactory.class) 注释Canvas 类中的pointData 字段即可。

【讨论】:

  • 完美!这正是我一直在寻找的。看起来我在尝试使用 TypeAdapterFactory 时有点接近,但我永远不会弄清楚所有类型的东西。我已经试过了,效果很好。非常感谢!
  • @natebot13 不客气。我刚刚还检查了您的 KeyedListDeserializer 实现,并且可以说出它为什么不起作用。类转换异常的主要原因和唯一原因是 new TypeToken&lt;T&gt;() 不像您想象的那样工作。基本上,它解析为的类型只是java.lang.Object,因此 Gson 没有足够的信息来获取对象的 real 类型。这就是为什么它选择“未知”目标对象反序列化器并从中生成链接树映射对象导致运行时异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多