Obtenha o objeto JSON aninhado com GSON usando retrofit


111

Estou consumindo uma API do meu aplicativo Android e todas as respostas JSON são assim:

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

O problema é que todos os meus POJOs têm um status, reasoncampos e dentro do contentcampo está o POJO real que desejo.

Existe alguma maneira de criar um conversor personalizado de Gson para extrair sempre o contentcampo, para que o retrofit retorne o POJO apropriado?



Eu li o documento, mas não vejo como fazê-lo ... :( Não sei como programar o código para resolver meu problema
mikelar

Estou curioso para saber por que você não formata sua classe POJO para lidar com esses resultados de status.
jj.

Respostas:


168

Você escreveria um desserializador personalizado que retornasse o objeto incorporado.

Digamos que seu JSON seja:

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

Você então teria um ContentPOJO:

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

Então você escreve um desserializador:

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

    }
}

Agora, se você construir um Gsoncom GsonBuildere registrar o desserializador:

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

Você pode desserializar seu JSON direto para Content:

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

Edite para adicionar a partir de comentários:

Se você tiver diferentes tipos de mensagens, mas todas elas tiverem o campo "conteúdo", poderá tornar o desserializador genérico fazendo:

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

    }
}

Você só precisa registrar uma instância para cada um dos seus tipos:

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

Quando você chama, .fromJson()o tipo é transportado para o desserializador, então deve funcionar para todos os seus tipos.

E, finalmente, ao criar uma instância de Retrofit:

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

1
Wow isso é ótimo! obrigado! : D Existe alguma maneira de generalizar a solução para que eu não precise criar um JsonDeserializer para cada tipo de resposta?
Mikelar de

1
Isso é incrível! Uma coisa a mudar: Gson gson = new GsonBuilder (). Create (); Em vez de Gson gson = new GsonBuilder (). Build (); Existem duas instâncias disso.
Nelson Osacky

7
@feresr você pode ligar setConverter(new GsonConverter(gson))na RestAdapter.Builderaula de Retrofit
akhy

2
@BrianRoach obrigado, boa resposta .. devo registrar Person.classe List<Person>.class/ Person[].classcom desserializador separado?
akhy

2
Alguma possibilidade de obter o "status" e o "motivo" também? Por exemplo, se todas as solicitações retornarem, podemos colocá-los em uma superclasse e usar subclasses que são os POJOs reais do "conteúdo"?
Nima G

14

A solução de @BrianRoach é a solução correta. É importante notar que, no caso especial em que você aninhou objetos personalizados que precisam de um personalizado TypeAdapter, você deve registrar o TypeAdaptercom a nova instância de GSON , caso contrário, o segundo TypeAdapternunca será chamado. Isso ocorre porque estamos criando uma nova Gsoninstância dentro de nosso desserializador personalizado.

Por exemplo, se você tivesse o seguinte json:

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

E você queria que este JSON fosse mapeado para os seguintes objetos:

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

class SubContent
{
    public String baz;
}

Você precisaria registrar o SubContent's TypeAdapter. Para ser mais robusto, você pode fazer o seguinte:

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

    }
}

e crie-o assim:

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

Isso poderia ser facilmente usado para o caso de "conteúdo" aninhado, simplesmente passando uma nova instância de MyDeserializer com valores nulos.


1
De qual pacote é "Type"? Existem um milhão de pacotes contendo a classe "Tipo". Obrigado.
Kyle Bridenstine

2
@ Mr.Tea Vai serjava.lang.reflect.Type
aidan

1
Onde está a classe SubContentDeserializer? @KMarlow
LogronJ

10

Um pouco tarde, mas espero que isso ajude alguém.

Basta criar o seguinte 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();
    }
}

e adicione-o ao seu construtor GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

ou

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

Isso é exatamente o que eu pareço. Porque eu tenho muitos tipos envolvidos com o nó de "dados" e não posso adicionar TypeAdapter a cada um deles. Obrigado!
Sergey Irisov

@SergeyIrisov de nada. Você pode votar nesta resposta para que ela fique mais alta :)
Matin Petrulak

Como passar vários jsonElement? como eu fiz content, content1etc.
Sathish Kumar J

Acho que seus devs back-end deveriam mudar a estrutura e não passar conteúdo, content1 ... Qual a vantagem dessa abordagem?
Matin Petrulak

Obrigado! Esta é a resposta perfeita. @Marin Petrulak: A vantagem é que este formulário está preparado para mudanças no futuro. "conteúdo" é o conteúdo da resposta. No futuro, podem surgir novos campos como "versão", "lastUpdated", "sessionToken" e assim por diante. Se você não agrupou o conteúdo da sua resposta de antemão, terá de enfrentar uma série de soluções alternativas no seu código para se adaptar à nova estrutura.
muetzenflo

7

Tive o mesmo problema alguns dias atrás. Resolvi isso usando a classe de wrapper de resposta e o transformador RxJava, que acho uma solução bastante flexível:

Embrulho:

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

Exceção personalizada para lançar, quando o status não está OK:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

Transformador Rx:

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

Exemplo de uso:

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

Meu tópico: Retrofit 2 RxJava - Gson - desserialização "Global", alterar tipo de resposta


Qual é a aparência do MyPojo?
IgorGanapolsky

1
@IgorGanapolsky MyPojo pode ter a aparência que você quiser. Deve corresponder aos dados de conteúdo recuperados de um servidor. A estrutura desta classe deve ser ajustada ao seu conversor de serialização (Gson, Jackson etc.).
rafakob

@rafakob, você também pode me ajudar com meu problema? Tenho dificuldade em tentar obter um campo em meu json aninhado da maneira mais simples possível. aqui está minha pergunta: stackoverflow.com/questions/56501897/…

6

Continuando a ideia de Brian, porque quase sempre temos muitos recursos REST, cada um com sua própria raiz, pode ser útil generalizar a desserialização:

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

    }
}

Então, para analisar a carga útil de amostra acima, podemos registrar o desserializador GSON:

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

3

Uma solução melhor poderia ser esta ..

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

Então, defina seu serviço assim ..

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

3

De acordo com a resposta de @Brian Roach e @rafakob eu fiz isso da seguinte maneira

Resposta Json do servidor

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

Classe de manipulador de dados comum

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Serializador personalizado

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

      }
}

Objeto Gson

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

Api call

 @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

Esta é a mesma solução que @AYarulin, mas assume que o nome da classe é o nome da chave JSON. Desta forma, você só precisa passar o nome da classe.

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

    }
}

Então, para analisar a carga útil de amostra acima, podemos registrar o desserializador GSON. Isso é problemático, pois a chave faz distinção entre maiúsculas e minúsculas, portanto, a caixa do nome da classe deve corresponder à caixa da chave JSON.

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

2

Aqui está uma versão do Kotlin baseada nas respostas de Brian Roach e AYarulin.

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

No meu caso, a chave "conteúdo" mudaria para cada resposta. Exemplo:

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

Nesses casos, usei uma solução semelhante à listada acima, mas tive que ajustá-la. Você pode ver a essência aqui . É um pouco grande para postar aqui no SOF.

A anotação @InnerKey("content")é usada e o resto do código é para facilitar seu uso com Gson.


Você também pode ajudar com a minha pergunta. Divirta-se obtendo um campo de um json aninhado da maneira mais simples possível: stackoverflow.com/questions/56501897/…


0

Outra solução simples:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.