Java 使用改造获取带有 GSON 的嵌套 JSON 对象

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23070298/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 20:26:53  来源:igfitidea点击:

Get nested JSON object with GSON using retrofit

javaandroidjsongsonretrofit

提问by mikelar

I'm consuming an API from my android app, and all the JSON responses are like this:

我正在从我的 android 应用程序中使用一个 API,所有的 JSON 响应都是这样的:

{
    'status': 'OK',
    'reason': 'Everything was fine',
    'content': {
         < some data here >
}

The problem is that all my POJOs have a status, reasonfields, and inside the contentfield is the real POJO I want.

问题是我所有的 POJO 都有一个status,reason字段,并且该content字段内部是我想要的真正的 POJO。

Is there any way to create a custom converter of Gson to extract always the contentfield, so retrofit returns the appropiate POJO?

有什么方法可以创建 Gson 的自定义转换器来始终提取该content字段,以便改造返回适当的 POJO?

采纳答案by Brian Roach

You would write a custom deserializer that returns the embedded object.

您将编写一个返回嵌入对象的自定义反序列化器。

Let's say your JSON is:

假设您的 JSON 是:

{
    "status":"OK",
    "reason":"some reason",
    "content" : 
    {
        "foo": 123,
        "bar": "some value"
    }
}

You'd then have a ContentPOJO:

然后你会有一个ContentPOJO:

class Content
{
    public int foo;
    public String bar;
}

Then you write a deserializer:

然后你写一个反序列化器:

class MyDeserializer implements JsonDeserializer<Content>
{
    @Override
    public Content deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
        throws JsonParseException
    {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        return new Gson().fromJson(content, Content.class);

    }
}

Now if you construct a Gsonwith GsonBuilderand register the deserializer:

现在,如果您构造一个GsonwithGsonBuilder并注册反序列化器:

Gson gson = 
    new GsonBuilder()
        .registerTypeAdapter(Content.class, new MyDeserializer())
        .create();

You can deserialize your JSON straight to your Content:

您可以将 JSON 直接反序列化为Content

Content c = gson.fromJson(myJson, Content.class);

Edit to add from comments:

编辑以从评论中添加:

If you have different types of messages but they all have the "content" field, you can make the Deserializer generic by doing:

如果您有不同类型的消息,但它们都有“内容”字段,您可以通过执行以下操作使反序列化器通用:

class MyDeserializer<T> implements JsonDeserializer<T>
{
    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
        throws JsonParseException
    {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        return new Gson().fromJson(content, type);

    }
}

You just have to register an instance for each of your types:

您只需要为每种类型注册一个实例:

Gson gson = 
    new GsonBuilder()
        .registerTypeAdapter(Content.class, new MyDeserializer<Content>())
        .registerTypeAdapter(DiffContent.class, new MyDeserializer<DiffContent>())
        .create();

When you call .fromJson()the type is carried into the deserializer, so it should then work for all your types.

当您调用.fromJson()该类型时,该类型会被带入解串器,因此它应该适用于您的所有类型。

And finally when creating a Retrofit instance:

最后在创建 Retrofit 实例时:

Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build();

回答by AYarulin

Continuing Brian's idea, because we almost always have many REST resources each with it's own root, it could be useful to generalize deserialization:

继续 Brian 的想法,因为我们几乎总是有许多 REST 资源,每个资源都有自己的根,因此概括反序列化可能很有用:

 class RestDeserializer<T> implements JsonDeserializer<T> {

    private Class<T> mClass;
    private String mKey;

    public RestDeserializer(Class<T> targetClass, String key) {
        mClass = targetClass;
        mKey = key;
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException {
        JsonElement content = je.getAsJsonObject().get(mKey);
        return new Gson().fromJson(content, mClass);

    }
}

Then to parse sample payload from above, we can register GSON deserializer:

然后从上面解析示例有效负载,我们可以注册 GSON 反序列化器:

Gson gson = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
    .build();

回答by KMarlow

@BrianRoach's solution is the correct solution. It is worth noting that in the special case where you have nested custom objects that both need a custom TypeAdapter, you must register the TypeAdapterwith the new instance of GSON, otherwise the second TypeAdapterwill never be called. This is because we are creating a new Gsoninstance inside our custom deserializer.

@BrianRoach 的解决方案是正确的解决方案。值得注意的是,在您嵌套的自定义对象都需要一个 custom 的特殊情况下TypeAdapter,您必须TypeAdapter使用GSON新实例注册,否则TypeAdapter将永远不会调用第二个。这是因为我们正在Gson自定义反序列化器中创建一个新实例。

For example, if you had the following json:

例如,如果您有以下 json:

{
    "status": "OK",
    "reason": "some reason",
    "content": {
        "foo": 123,
        "bar": "some value",
        "subcontent": {
            "useless": "field",
            "data": {
                "baz": "values"
            }
        }
    }
}

And you wanted this JSON to be mapped to the following objects:

您希望将此 JSON 映射到以下对象:

class MainContent
{
    public int foo;
    public String bar;
    public SubContent subcontent;
}

class SubContent
{
    public String baz;
}

You would need to register the SubContent's TypeAdapter. To be more robust, you could do the following:

您需要注册SubContentTypeAdapter. 为了更健壮,您可以执行以下操作:

public class MyDeserializer<T> implements JsonDeserializer<T> {
    private final Class mNestedClazz;
    private final Object mNestedDeserializer;

    public MyDeserializer(Class nestedClazz, Object nestedDeserializer) {
        mNestedClazz = nestedClazz;
        mNestedDeserializer = nestedDeserializer;
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
        // Get the "content" element from the parsed JSON
        JsonElement content = je.getAsJsonObject().get("content");

        // Deserialize it. You use a new instance of Gson to avoid infinite recursion
        // to this deserializer
        GsonBuilder builder = new GsonBuilder();
        if (mNestedClazz != null && mNestedDeserializer != null) {
            builder.registerTypeAdapter(mNestedClazz, mNestedDeserializer);
        }
        return builder.create().fromJson(content, type);

    }
}

and then create it like so:

然后像这样创建它:

MyDeserializer<Content> myDeserializer = new MyDeserializer<Content>(SubContent.class,
                    new SubContentDeserializer());
Gson gson = new GsonBuilder().registerTypeAdapter(Content.class, myDeserializer).create();

This could easily be used for the nested "content" case as well by simply passing in a new instance of MyDeserializerwith null values.

通过简单地传入一个MyDeserializer空值的新实例,这也可以很容易地用于嵌套的“内容”情况。

回答by Barry MSIH

This is the same solution as @AYarulin but assume the class name is the JSON key name. This way you only need to pass the Class name.

这与@AYarulin 的解决方案相同,但假设类名是 JSON 键名。这样你只需要传递类名。

 class RestDeserializer<T> implements JsonDeserializer<T> {

    private Class<T> mClass;
    private String mKey;

    public RestDeserializer(Class<T> targetClass) {
        mClass = targetClass;
        mKey = mClass.getSimpleName();
    }

    @Override
    public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
            throws JsonParseException {
        JsonElement content = je.getAsJsonObject().get(mKey);
        return new Gson().fromJson(content, mClass);

    }
}

Then to parse sample payload from above, we can register GSON deserializer. This is problematic as the Key is case sensitive, so the case of the class name must match the case of the JSON key.

然后从上面解析示例有效负载,我们可以注册 GSON 解串器。这是有问题的,因为 Key 区分大小写,因此类名的大小写必须与 JSON 键的大小写匹配。

Gson gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();

回答by rafakob

Had the same problem couple of days ago. I've solve this using response wrapper class and RxJava transformer, which I think is quite flexiable solution:

前几天遇到同样的问题。我已经使用响应包装器类和 RxJava 转换器解决了这个问题,我认为这是非常灵活的解决方案:

Wrapper:

包装:

public class ApiResponse<T> {
    public String status;
    public String reason;
    public T content;
}

Custom exception to throw, when status is not OK:

自定义异常抛出,当状态不正常时:

public class ApiException extends RuntimeException {
    private final String reason;

    public ApiException(String reason) {
        this.reason = reason;
    }

    public String getReason() {
        return apiError;
    }
}

Rx transformer:

接收变压器:

protected <T> Observable.Transformer<ApiResponse<T>, T> applySchedulersAndExtractData() {
    return observable -> observable
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .map(tApiResponse -> {
                if (!tApiResponse.status.equals("OK"))
                    throw new ApiException(tApiResponse.reason);
                else
                    return tApiResponse.content;
            });
}

Example usage:

用法示例:

// Call definition:
@GET("/api/getMyPojo")
Observable<ApiResponse<MyPojo>> getConfig();

// Call invoke:
webservice.getMyPojo()
        .compose(applySchedulersAndExtractData())
        .subscribe(this::handleSuccess, this::handleError);


private void handleSuccess(MyPojo mypojo) {
    // handle success
}

private void handleError(Throwable t) {
    getView().showSnackbar( ((ApiException) throwable).getReason() );
}

My topic: Retrofit 2 RxJava - Gson - "Global" deserialization, change response type

我的话题: Retrofit 2 RxJava - Gson - “全局”反序列化,改变响应类型

回答by Varun Achar

In my case, the "content" key would change for each response. Example:

就我而言,每个响应的“内容”键都会改变。例子:

// Root is hotel
{
  status : "ok",
  statusCode : 200,
  hotels : [{
    name : "Taj Palace",
    location : {
      lat : 12
      lng : 77
    }

  }, {
    name : "Plaza", 
    location : {
      lat : 12
      lng : 77
    }
  }]
}

//Root is city

{
  status : "ok",
  statusCode : 200,
  city : {
    name : "Vegas",
    location : {
      lat : 12
      lng : 77
    }
}

In such cases I used a similar solution as listed above but had to tweak it. You can see the gist here. It's a little too large to post it here on SOF.

在这种情况下,我使用了上面列出的类似解决方案,但不得不对其进行调整。你可以在这里看到要点。把它张贴在 SOF 上有点太大了。

The annotation @InnerKey("content")is used and the rest of the code is to facilitate it's usage with Gson.

使用了注解@InnerKey("content"),其余的代码是为了方便它与 Gson 的使用。

回答by Sayed Abolfazl Fatemi

Don't forget @SerializedNameand @Exposeannotations for all Class members and Inner Class members that most deserialized from JSON by GSON.

不要忘了@SerializedName,并@Expose为所有类成员和嵌套类的成员,大部分由GSON从JSON反序列化的注释。

Look at https://stackoverflow.com/a/40239512/1676736

https://stackoverflow.com/a/40239512/1676736

回答by Federico J Farina

A better solution could be this..

更好的解决方案可能是这样..

public class ApiResponse<T> {
    public T data;
    public String status;
    public String reason;
}

Then, define your service like this..

然后,像这样定义您的服务..

Observable<ApiResponse<YourClass>> updateDevice(..);

回答by Matin Petrulak

Bit late but hopefully this will help someone.

有点晚了,但希望这会对某人有所帮助。

Just create following TypeAdapterFactory.

只需创建以下 TypeAdapterFactory。

    public class ItemTypeAdapterFactory implements TypeAdapterFactory {

      public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {

        final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
        final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);

        return new TypeAdapter<T>() {

            public void write(JsonWriter out, T value) throws IOException {
                delegate.write(out, value);
            }

            public T read(JsonReader in) throws IOException {

                JsonElement jsonElement = elementAdapter.read(in);
                if (jsonElement.isJsonObject()) {
                    JsonObject jsonObject = jsonElement.getAsJsonObject();
                    if (jsonObject.has("content")) {
                        jsonElement = jsonObject.get("content");
                    }
                }

                return delegate.fromJsonTree(jsonElement);
            }
        }.nullSafe();
    }
}

and add it into your GSON builder :

并将其添加到您的 GSON 构建器中:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

or

或者

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

回答by RamwiseMatt

Here's a Kotlin version based on the answers by Brian Roach and AYarulin.

这是基于 Brian Roach 和 AYarulin 的答案的 Kotlin 版本。

class RestDeserializer<T>(targetClass: Class<T>, key: String?) : JsonDeserializer<T> {
    val targetClass = targetClass
    val key = key

    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): T {
        val data = json!!.asJsonObject.get(key ?: "")

        return Gson().fromJson(data, targetClass)
    }
}