可以避免使用JSON包装器的“单入口”映射吗?

时间:2017-01-18 13:31:55

标签: java json rest gson retrofit

我在使用Retrofit2编写REST接口的存根时看到的常见模式通常是,实际参数需要用带有单个条目的映射包装(或者,更糟糕的是,包含一个类的包装类的包装类字段)。

典型的JSON有效负载看起来像{"idontcareaboutthis": { // Data I actually want...。有没有办法剥掉这个相对无用的外壳?我觉得我所有的REST方法都有一个返回类型的Map。

1 个答案:

答案 0 :(得分:1)

你不需要地图。您可以编写自己的JSON反序列化程序。让我们说,你有以下JSON,你不需要根对象单键:

{
    "idontcareaboutthis": {
        "foo": 1,
        "bar": 2
    }
}

然后JSON反序列化器可能如下所示:

final class ResponseJsonDeserializer<T>
        implements JsonDeserializer<T> {

    private final Gson backingGson;

    private ResponseJsonDeserializer(final Gson backingGson) {
        this.backingGson = backingGson;
    }

    static <T> JsonDeserializer<T> getResponseJsonDeserializer(final Gson backingGson) {
        return new ResponseJsonDeserializer<>(backingGson);
    }

    @Override
    public T deserialize(final JsonElement json, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        final JsonObject root = json.getAsJsonObject();
        final Set<Entry<String, JsonElement>> entries = root.entrySet();
        final int propertyCount = entries.size();
        if ( propertyCount != 1 ) {
            throw new JsonParseException("Expected a single property root object, but got an object with " + propertyCount + " properties");
        }
        final Entry<String, JsonElement> inner = entries.iterator().next();
        // Can't use context.deserialize here due to recursion
        return backingGson.fromJson(inner.getValue(), type);
    }

}

注意上面的反序列化器如何提取根对象条目以及它如何将反序列化过程委托给另一个Gson实例。现在,您必须创建一个了解idontcareaboutthis属性的Gson实例。

    private static final Gson registryBackingGson = new GsonBuilder()
            // whatever necessary here
            .create();

    private static final Gson registryGson = new GsonBuilder()
            .registerTypeAdapter(FooBarResponse.class, getResponseJsonDeserializer(registryBackingGson))
            // add another response classes here like the above, but do not register other types - they must be registered in registryBackingGson
            .create();

registryGson要求枚举所有响应类或注册特定类型层次结构。如果第一种情况对您来说不方便,并且您可以更改响应类源代码,则可以添加特殊标记接口以注册整个类型层次结构。说,像这样:

    private static final Gson registryBackingGson = new GsonBuilder()
            // whatever necessary here
            .create();

    private static final Gson registryGson = new GsonBuilder()
            .registerTypeHierarchyAdapter(IResponse.class, getResponseJsonDeserializer(registryBackingGson))
            // no need to add another "response" classes here - they just must implement the marker interface
            .create();

数据传输对象:

final class FooBarResponse {

    // The `final` modifier is a reasonable habit for incoming DTO classes, but primitive constants are inlined by the compiler.
    // Suppressing the inlining can be done be a simple workaround to make javac think that it's not a real constant.
    // However, it's a matter of your code style, and this is just an example.
    private final int foo = constOf(0);
    private final int bar = constOf(0);

    int getFoo() {
        return foo;
    }

    int getBar() {
        return bar;
    }

    // We're cheating...
    private static int constOf(final int i) {
        return i;
    }

}

如果您更喜欢标记界面并注册整个类型层次结构

interface IResponse {
}

final class FooBarResponse
        implements IResponse {
...

它是如何运作的:

final FooBarResponse fooBar = registryGson.fromJson(JSON, FooBarResponse.class)
out.println(fooBar.getFoo()); // 1
out.println(fooBar.getBar()); // 2

改装适配器:

final Retrofit retrofit = new Retrofit.Builder()
        // ...
        .addConverterFactory(GsonConverterFactory.create(registryGson))
        .build();

因此,您的基于Retrofit的接口方法可以返回FooBar / etc类实例而不是映射。