Java:将未知规范的嵌套 JSON 反序列化为多态哈希表

Java: Deserialize nested JSON of unknown spec to a polymorphic Hashtable

我想将具有 unknown/varied 字段名称和结构的嵌套 JSON 映射转换为通用和多态 Java 数据结构。我相信 HashMap<String,Object> 包含我的案例的这个要求。

澄清:多态仅指 String、HashMap 或 ArrayList。

这是一个例子JSON,请注意键名和结构是可变的:

{"k1":{"k2":["1","2"], "k3":{"k4":"v4","k5":"v5"}, "k6":"v6"}

我已经用 Gson 实现了它,它在创建嵌套的 Java 哈希映射时起作用,但它试图将值转换为 Java 基本 类,例如Double、Integer 等,它不仅做得非常糟糕(例如时间戳长到 Double),而且没有必要,因为我需要的只是将所有值转换为 String,除非它们是映射或数组。我猜 Object 被拿得太大方了。我用过这个:

@SerializedName("data")
HashMap<String,Object> data;

我在这里问过一个问题Retrofit2, Gson, Java: Parse received JSON values as String, only,它收集了反对票,但没有一点智慧。

现在我正在尝试使用 Moshi,例如:

@JsonClass(generateAdapter = true)
public HashMap<String, Object> data;

但仍然没有快乐,因为它抱怨 java.lang.IllegalArgumentException: Unable to create converter ...

所以,我正在寻找有关此问题的指导。我很简单,我不需要将 JSON 转换为 POJO。对于此实例,将 JSON 转换为 HashMap<String,Object> 就足够了。我在工具上很容易使用:Gson,Moshi 或者我自己定制的JSON -> 嵌套多态HashMap。有任何指示或想法吗?

在 Gson 中,无法覆盖 Object 类型适配器,因此没有直接的方法来反序列化通用类型下的字符串、列表和映射。但是,您可以假装它们三个可以有一个共同的超类型,通过引入标记类型来欺骗类型系统,例如仅标记某些共同点的注释。伪造的通用类型标记仅适用于顶级 fromJson 调用,不适用于真正强类型的字段,但是您不需要它们,正如您在问题中提到的那样。

@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Poly {
}
public final class PolyTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory defaultInstance = new PolyTypeAdapterFactory(ArrayList::new, LinkedHashMap::new);

    private final Supplier<? extends List<? super Object>> createList;
    private final Supplier<? extends Map<String, ? super Object>> createMap;

    private PolyTypeAdapterFactory(final Supplier<? extends List<? super Object>> createList, final Supplier<? extends Map<String, ? super Object>> createMap) {
        this.createList = createList;
        this.createMap = createMap;
    }

    public static TypeAdapterFactory getDefaultInstance() {
        return defaultInstance;
    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( typeToken.getRawType() != Poly.class ) {
            return null;
        }
        final TypeAdapter<Object> polyTypeAdapter = new TypeAdapter<Object>() {
            @Override
            public void write(final JsonWriter out, final Object o) {
                throw new UnsupportedOperationException();
            }

            @Override
            public Object read(final JsonReader in)
                    throws IOException {
                final JsonToken token = in.peek();
                switch ( token ) {
                case BEGIN_ARRAY:
                    return readList(in);
                case BEGIN_OBJECT:
                    return readMap(in);
                case STRING:
                case NUMBER:
                case BOOLEAN:
                    return readString(in);
                case NULL:
                    return readNull(in);
                case NAME:
                case END_ARRAY:
                case END_OBJECT:
                    throw new AssertionError("Unexpected token: " + token + " at " + in);
                case END_DOCUMENT:
                    throw new UnsupportedOperationException();
                default:
                    throw new AssertionError(token);
                }
            }

            @Nullable
            private Object readNull(final JsonReader in)
                    throws IOException {
                in.nextNull();
                return null;
            }

            private String readString(final JsonReader in)
                    throws IOException {
                return in.nextString();
            }

            private Map<String, Object> readMap(final JsonReader in)
                    throws IOException {
                in.beginObject();
                final Map<String, Object> map = createMap.get();
                while ( in.hasNext() ) {
                    final String key = in.nextName();
                    if ( map.containsKey(key) ) {
                        throw new IllegalStateException("Duplicate key: " + key);
                    }
                    @Nullable
                    final Object value = read(in);
                    if ( value != null ) {
                        map.put(key, value);
                    }
                }
                in.endObject();
                return map;
            }

            private List<Object> readList(final JsonReader in)
                    throws IOException {
                in.beginArray();
                final List<Object> list = createList.get();
                while ( in.hasNext() ) {
                    final Object element = read(in);
                    list.add(element);
                }
                in.endArray();
                return list;
            }
        }
                .nullSafe();
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) polyTypeAdapter;
        return typeAdapter;
    }

}

上面的类型适配器检查给定的类型是否是伪超级类型,然后根据实际的 JSON 标记反序列化为三种类型:

  • 每个简单文字的原始字符串(Gson nextString 强制数字和布尔值);
  • 容器的映射和列表,递归。

我稍微修改了你的 JSON 以表明数字将被强制转换为字符串:

{
    "k1": {
        "k2": [
            "1",
            2
        ],
        "k3": {
            "k4": "v4",
            "k5": "v5"
        },
        "k6": "v6"
    }
}

以下测试将通过:

public final class PolyTypeAdapterFactoryTest {

    private static final Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .disableInnerClassSerialization()
            .registerTypeAdapterFactory(PolyTypeAdapterFactory.getDefaultInstance())
            .create();

    @Test
    public void test()
            throws IOException {
        try ( final JsonReader jsonReader = new JsonReader(...) ) {
            // This is where cheating happens: we tell Gson to apply the Poly handler,
            // but it returns a Map. Well, let it be for the top-most level...
            final Map<String, ?> actual = gson.fromJson(jsonReader, Poly.class);
            final Map<String, ?> expected = ImmutableMap.of(
                    "k1", ImmutableMap.of(
                            "k2", ImmutableList.of("1", "2"),
                            "k3", ImmutableMap.of(
                                    "k4", "v4",
                                    "k5", "v5"
                            ),
                            "k6", "v6"
                    )
            );
            Assertions.assertEquals(expected, actual);
        }
    }

}

没有 Gson 但 org.json 解决方案

正如我在评论中提到的,您可以避免使用 Gson de/serialization 工具,并完全手动反序列化给定的 JSON:

public final class OrgJsonPolyReader {

    private OrgJsonPolyReader() {
    }

    @Nullable
    public static Object read(final JSONTokener jsonTokener) {
        return read(jsonTokener, ArrayList::new, LinkedHashMap::new);
    }

    @Nullable
    public static Object read(final JSONTokener jsonTokener, final Supplier<? extends List<? super Object>> createList,
            final Supplier<? extends Map<? super String, ? super Object>> createMap) {
        while ( jsonTokener.more() ) {
            final char token = jsonTokener.nextClean();
            switch ( token ) {
            case 'n':
                jsonTokener.back();
                return readNull(jsonTokener);
            case 'f':
            case 't':
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
                jsonTokener.back();
                return readLiteral(jsonTokener);
            case '"':
                jsonTokener.back();
                return readString(jsonTokener);
            case '[':
                jsonTokener.back();
                return readList(jsonTokener, createList, createMap);
            case '{':
                jsonTokener.back();
                return readMap(jsonTokener, createList, createMap);
            default:
                throw new IllegalStateException("Unexpected token: " + token);
            }
        }
        throw new AssertionError();
    }

    @Nullable
    private static <T> T readNull(final JSONTokener jsonTokener) {
        final Object value = jsonTokener.nextValue();
        assert value.equals(JSONObject.NULL);
        return null;
    }

    private static String readLiteral(final JSONTokener jsonTokener) {
        return jsonTokener.nextValue().toString();
    }

    private static String readString(final JSONTokener jsonTokener) {
        jsonTokener.next('"');
        return jsonTokener.nextString('"');
    }

    private static List<Object> readList(final JSONTokener jsonTokener, final Supplier<? extends List<? super Object>> createList,
            final Supplier<? extends Map<? super String, ? super Object>> createMap) {
        jsonTokener.next('[');
        final List<? super Object> list = createList.get();
        for ( ; ; ) {
            final char token = jsonTokener.nextClean();
            switch ( token ) {
            case ']':
                return list;
            case ',':
                break;
            default:
                jsonTokener.back();
                final Object value = read(jsonTokener, createList, createMap);
                list.add(value);
                break;
            }
        }
    }

    private static Map<? super String, Object> readMap(final JSONTokener jsonTokener, final Supplier<? extends List<? super Object>> createList,
            final Supplier<? extends Map<? super String, ? super Object>> createMap) {
        jsonTokener.next('{');
        final Map<? super String, ? super Object> map = createMap.get();
        for ( ; ; ) {
            final char token = jsonTokener.nextClean();
            switch ( token ) {
            case '}':
                return map;
            case ',':
                break;
            case '"':
                final String key = jsonTokener.nextString(token);
                if ( map.containsKey(key) ) {
                    throw new IllegalStateException("Duplicate key: " + key);
                }
                jsonTokener.next(':');
                final Object value = read(jsonTokener, createList, createMap);
                map.put(key, value);
                break;
            default:
                throw new IllegalStateException("Unexpected token: " + token);
            }
        }
    }

}

这就是它可以添加到 Retrofit 而不是 Gson 转换器工厂的方式:

.addConverterFactory(new Converter.Factory() {
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
        return responseBody -> {
            try ( final Reader reader = new InputStreamReader(responseBody.byteStream()) ) {
                return OrgJsonPolyReader.read(new JSONTokener(reader));
            }
        };
    }
})

呼,这是一个漫长的旅程。希望这将帮助那些想要避免使用 GsonMoshi 并有一个简单的 JSON 解析器将 JSON 字符串转换为多态(多类型?)LinkedHashMap。多态性是指它仅包含 StringArrayMap。我个人更喜欢它而不是大量的 POJO。上面的@Fluffy 演示了如何使用 Gson 创建所述多态 Map。但同时删除 Gson 并使用 org.json 也很诱人。 @Fluffy 也提供了解决方案。

所以,同样,这个解决方案基于@Fluffy 上面的回答,但我还添加了创建一个在返回的 Map 上构造的自定义对象(这样我就不会继续对某些字段进行类型转换)。我刚刚修改了这个方法(BaseResponse3是我的容器class):

    public static Object read(final JSONTokener jsonTokener) {
        Log.d(TAG, "read()/1 : called ...");
        Map<? super String, ? super Object> ret = (Map) (read(
                jsonTokener, ArrayList::new, LinkedHashMap::new
        ));
        BaseResponse3 br = new BaseResponse3(
                Integer.parseInt((String )(ret.get("status"))),
                (String )(ret.get("message")),
                (HashMap)(ret.get("data"))
        );
        Log.d(TAG, "EOF reached: " + br);
        return br;
    }

我处理的第二件事是在... android 中使用此代码。哎哟。似乎 android 内置 json.org 支持相同的命名空间。它有所修改。例如,它没有 JSONTokener(Reader ...) 构造函数。这是我发布的一个问题:org.json.JSONTokener : no constructor from Reader

所以我不得不使用一个“真实的”org.json(在我的例子中是 https://github.com/stleary/JSON-java),我不得不将它的命名空间修改为 org.json.XYZ 并创建一个 JAR(带有我的 linux 朋友几乎没有帮助我实现一切自动化)。该 JAR 包含在 Android Studio 中,方法是将其添加到 build.gradle(项目):

        flatDir {
            dirs '../../3rdparty/jars'
        }

并将此添加到 build.gradle(应用程序):

implementation fileTree(include: ['*.jar'], dir: '../../3rdparty/jars')

我现在按照@Fluffy 的说明将此 JSON 解析器合并到 Retrofit 中,使用:

        Retrofit myRetrofitWithJsonOrg = new Retrofit.Builder()
                .baseUrl(base_url)
                .client(myOkHttpClient)
                .addConverterFactory(
                        new Converter.Factory() {
                            @Override
                            public Converter<ResponseBody, ?> responseBodyConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
                                return responseBody -> {
                                    try (final Reader reader = new InputStreamReader(responseBody.byteStream())) {
                                        return OrgJsonDeserializer.read(new org.json.XYZ.JSONTokener(reader));
                                    }
                                };
                            }
                        })
                .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
                .build();

一切正常。

尽管很可能存在性能问题(到目前为止我还没有注意到)因为 Android 的 org.json 可能会以某种方式被修改比香草 org.json 更有效地处理 JSON 数据流。或 Gson 以更有效地与 OkHttp 交互。我很高兴我已经摆脱了另一个依赖的魔爪,我很难告诉它我希望它如何表现。例如,我没能告诉它 ^&^%& 停止将每个 JSON 字符串转换为 Java 基本类型。我结束了将整数转换为浮点数和时间戳(长)的荒谬状态...... Double!!!!!使用 org.json 我完全可以控制。谢谢@Fluffy。