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


111

我正在使用Android应用程序中的API,所有JSON响应均如下所示:

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

问题是,我所有的POJO有statusreason字段,里面content领域是真正的POJO我想要的。

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



我阅读了文档,但看不到该怎么做... :(我不知道如何编写代码来解决我的问题
mikelar 2014年

我很好奇为什么您不只是格式化POJO类来处理这些状态结果。
jj。

Answers:


168

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

假设您的JSON是:

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

然后,您将获得一个ContentPOJO:

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

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

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

    }
}

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

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

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

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

编辑以添加评论:

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

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

    }
}

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

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

当您调用.fromJson()该类型时,该类型将携带到反序列化器中,因此它应适用于所有类型。

最后,在创建Retrofit实例时:

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

1
哇,太好了!谢谢!:D有没有什么办法可以概括该解决方案,所以我不必为每种响应类型都创建一个JsonDeserializer?
mikelar

1
这真太了不起了!更改的一件事:Gson gson = new GsonBuilder()。create(); 代替Gson gson = new GsonBuilder()。build(); 有两种情况。
Nelson Osacky 2014年

7
@feresr你可以调用setConverter(new GsonConverter(gson))在改造的RestAdapter.Builder
akhy

2
@BrianRoach谢谢,很好的答案..我应该用单独的Deserializer 注册Person.classList<Person>.class/ Person[].class吗?
akhy 2014年

2
是否也有可能获得“状态”和“原因”?例如,如果所有请求都返回了它们,我们是否可以将它们放在超类中,并使用子类(它们是“内容”中的实际POJO)?
尼玛·G

14

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

例如,如果您具有以下json:

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

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

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

class SubContent
{
    public String baz;
}

您需要注册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);

    }
}

然后像这样创建它:

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

通过简单地传入MyDeserializer具有空值的新实例,也可以轻松地将其用于嵌套的“内容”情况。


1
什么是“类型”包?有上百万个包含“类型”类的软件包。谢谢。
凯尔·布​​莱恩斯汀

2
@ Mr.Tea将会是java.lang.reflect.Type
艾丹2015年

1
SubContentDeserializer类在哪里?@KMarlow
LogronJ

10

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

只需创建以下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();
    }
}

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

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

要么

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

这正是我的样子。因为我有很多用“数据”节点包装的类型,所以不能向每个类型添加TypeAdapter。谢谢!
谢尔盖·伊里索夫

@SergeyIrisov不客气。您可以投票给这个答案,让它变得更高:)
Matin Petrulak

如何通过多个jsonElement?像我有contentcontent1
Sathish所在库马尔Ĵ

我认为您的后端开发人员应更改结构,并且不要传递内容,内容1 ...这种方法的优点是什么?
Matin Petrulak '17

谢谢!这是完美的答案。@Marin Petrulak:好处是这种形式可以适应未来的变化。“内容”是响应内容。将来,它们可能会出现新的字段,例如“版本”,“ lastUpdated”,“ sessionToken”等。如果您没有事先包装响应内容,那么您的代码中就会遇到很多变通办法,以适应新的结构。
muetzenflo

7

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

包装器:

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

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

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

接收变压器:

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

用法示例:

// 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() );
}

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


MyPojo是什么样的?
IgorGanapolsky

1
@IgorGanapolsky MyPojo可以根据需要显示。它应与您从服务器检索到的内容数据匹配。此类的结构应根据您的序列化转换器(Gson,Jackson等)进行调整。
rafakob

@rafakob还可以帮我解决我的问题吗?尝试以最简单的方式尝试在嵌套的json中获取字段非常困难。这是我的问题:stackoverflow.com/questions/56501897/…–

6

继续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);

    }
}

然后从上方解析样本有效负载,我们可以注册GSON解串器:

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

3

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

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

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

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

3

根据@Brian Roach和@rafakob的回答,我以以下方式完成此操作

服务器的Json响应

{
  "status": true,
  "code": 200,
  "message": "Success",
  "data": {
    "fullname": "Rohan",
    "role": 1
  }
}

通用数据处理程序类

public class ApiResponse<T> {
    @SerializedName("status")
    public boolean status;

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

    @SerializedName("data")
    public T content;
}

自定义序列化器

static class MyDeserializer<T> implements JsonDeserializer<T>
{
     @Override
      public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
                    throws JsonParseException
      {
          JsonElement content = je.getAsJsonObject();

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

      }
}

Gson对象

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

Api电话

 @FormUrlEncoded
 @POST("/loginUser")
 Observable<ApiResponse<Profile>> signIn(@Field("email") String username, @Field("password") String password);

restService.signIn(username, password)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeOn(Schedulers.io())
                .subscribe(new Observer<ApiResponse<Profile>>() {
                    @Override
                    public void onCompleted() {
                        Log.i("login", "On complete");
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.i("login", e.toString());
                    }

                    @Override
                    public void onNext(ApiResponse<Profile> response) {
                         Profile profile= response.content;
                         Log.i("login", profile.getFullname());
                    }
                });

2

此解决方案与@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);

    }
}

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

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

2

这是基于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)
    }
}

1

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

// 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
    }
}

在这种情况下,我使用了上面列出的类似解决方案,但必须对其进行调整。你可以在这里看到要点。它太大了,无法在SOF上发布。

使用了注释@InnerKey("content"),其余的代码是为了便于在Gson中使用。


你能帮我解决我的问题吗?想要用最简单的方法从嵌套的json获取字段的时间是:stackoverflow.com/questions/56501897/…–


0

另一个简单的解决方案:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.