gson Java:将未知规范的嵌套JSON反序列化为多态Hashtable

bmvo0sr5  于 2022-11-23  发布在  Java
关注(0)|答案(2)|浏览(188)

我想将具有未知/可变字段名和结构嵌套JSONMap转换为通用和多态Java数据结构。

澄清:多态仅表示字符串、HashMap或数组列表。

下面是一个JSON示例,请注意key-names和structure是变量:

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

我已经用Gson实现了它,它用于创建嵌套的Java散列表,但它试图将值转换为Java基本类,例如Double,整数等等,这不仅它做得很不好(例如,timestamp long到Double),但这是不必要的,因为我所需要的只是将所有值转换为String,除非它们是Map或数组。我猜Object被取得太自由了。我用了这个:

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

我在这里问了一个关于这个问题的问题Retrofit2, Gson, Java: Parse received JSON values as String, only,它收集了反对票,但没有一点点智慧。
现在,我尝试使用莫希,内容如下:

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

但仍然没有喜悦,因为它抱怨java.lang.IllegalArgumentException: Unable to create converter ...
所以,我在这个问题上寻找指导。我很容易,我不需要把JSON转换成POJO。对于这个例子,把JSON转换成HashMap<String,Object>就足够了。我很容易使用这个工具:Gson,莫希或者我自己定制的JSON -〉嵌套的多态HashMap。有什么建议或者想法吗?

zaq34kh6

zaq34kh61#

在Gson中,不可能覆盖Object类型适配器,因此没有直接的方法来反序列化公共类型下的字符串、列表和Map。但是,您可以假设它们三个可以有一个公共的超类型,通过引入标记类型来欺骗类型系统,例如,只标记共同之处的注解。虚设的通用型别标记只会作用于最上层的fromJson呼叫,而不会我不适用于真正强类型的字段,但正如您在问题中提到的,您并不需要它们。
第一个
上面的类型适配器检查给定的类型是否是伪超类型,然后根据实际的JSON标记反序列化为三种类型:

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

我稍微修改了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反序列化/序列化工具,并完全手动地反序列化给定的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));
            }
        };
    }
})
icnyk63a

icnyk63a2#

希望这能帮助那些不想使用GsonMoshi,而想用一个简单的JSON解析器将JSON字符串转换为多态的(多型?)LinkedHashMap。多态性是指它只包含StringArrayMap。我个人更喜欢这一点,而不是大量的POJO。@Fluffy上面演示了如何使用Gson创建上述多态Map。但是,同时删除Gson并使用org.json也是很有诱惑力的。@Fluffy也提供了这种解决方案。
因此,同样,这个解决方案是基于@Fluffy上面的答案,但我还添加了创建一个在返回的Map上构造的自定义对象(这样我就不会一直对某些字段进行类型转换)。我刚刚修改了这个方法(BaseResponse3是我的容器类):

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添加到build.gradle(项目)中,该JAR被包含在Android Studio中:

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.jsonGson更高效地处理JSON数据流,从而更高效地与OkHttp交互。我很高兴我从另一个依赖的魔爪中逃脱了,我没有设法告诉它 ^&^%& 停止将每个JSON字符串转换为Java基本类型。我以一种荒谬的状态结束了,将整数转换为浮点数,将时间戳(长)转换为...双精度!!!!!使用org.json,我完全控制了。谢谢@Fluffy。

相关问题