web-dev-qa-db-fra.com

Obtention d'objet JSON imbriqué avec GSON à l'aide de la modification

Je consomme une API de mon application Android et toutes les réponses JSON ressemblent à ceci:

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

Le problème est que tous mes POJO ont des champs status, reason et que le champ content contient le véritable POJO que je veux.

Existe-t-il un moyen de créer un convertisseur personnalisé de Gson pour extraire toujours le champ content, de sorte que retrofit renvoie le POJO approprié?

96
mikelar

Vous écririez un désérialiseur personnalisé qui renverrait l'objet incorporé.

Disons que votre JSON est:

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

Vous auriez alors un Content POJO:

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

Ensuite, vous écrivez un désérialiseur:

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

    }
}

Maintenant, si vous construisez une Gson avec GsonBuilder et enregistrez le désérialiseur:

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

Vous pouvez désérialiser votre JSON directement dans votre Content:

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

Modifier pour ajouter des commentaires:  

Si vous avez différents types de messages mais qu'ils ont tous le champ "contenu", vous pouvez rendre générique le désérialiseur en procédant comme suit:

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

    }
}

Vous devez simplement enregistrer une instance pour chacun de vos types:

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

Lorsque vous appelez .fromJson(), le type est transféré dans le désérialiseur, il devrait donc fonctionner pour tous vos types. 

Et enfin, lors de la création d'une instance Retrofit:

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

La solution @ BrianRoach est la bonne solution. Il convient de noter que dans le cas particulier où vous avez des objets personnalisés imbriqués nécessitant tous les deux une variable TypeAdapter, vous devez enregistrer la variable TypeAdapter avec l'instance new de GSON, sinon la seconde TypeAdapter ne sera jamais appelée. En effet, nous créons une nouvelle instance Gson dans notre désérialiseur personnalisé.

Par exemple, si vous aviez le json suivant:

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

Et vous vouliez que ce JSON soit associé aux objets suivants:

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

class SubContent
{
    public String baz;
}

Vous devez enregistrer la variable SubContent 's TypeAdapter. Pour être plus robuste, vous pouvez procéder comme suit:

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

    }
}

puis créez-le comme ceci:

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

Cela pourrait facilement être utilisé pour le cas "contenu" imbriqué également en transmettant simplement une nouvelle instance de MyDeserializer avec des valeurs null.

15
KMarlow

Un peu tard, mais j'espère que cela aidera quelqu'un.

Il suffit de créer ce qui suit 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();
    }
}

et l'ajouter à votre constructeur GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

ou

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
9
Matin Petrulak

En continuant l’idée de Brian, comme nous avons presque toujours de nombreuses ressources REST ayant chacune sa propre racine, il pourrait être utile de généraliser la désérialisation:

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

    }
}

Ensuite, pour analyser un exemple de charge utile ci-dessus, nous pouvons enregistrer le désérialiseur GSON:

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

Avait le même problème il y a quelques jours. J'ai résolu ce problème en utilisant la classe de wrapper de réponse et le transformateur RxJava, ce qui me semble être une solution assez flexible:

Wrapper:

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

Exception personnalisée à émettre, lorsque l'état n'est pas OK:

public class ApiException extends RuntimeException {
    private final String reason;

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

    public String getReason() {
        return apiError;
    }
}

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

Exemple d'utilisation:

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

Mon sujet: Retrofit 2 RxJava - Gson - Désérialisation "globale", changement du type de réponse

6
rafakob

D'après les réponses de @Brian Roach et @rafakob, j'ai procédé de la manière suivante

Réponse Json du serveur

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

Classe de gestionnaire de données commune

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Sérialiseur personnalisé

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

      }
}

Objet Gson

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

Appel 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());
                    }
                });
3
Rohan Pawar

Une meilleure solution pourrait être ceci ..

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

Ensuite, définissez votre service comme ceci.

Observable<ApiResponse<YourClass>> updateDevice(..);
3
Federico J Farina

Voici une version de Kotlin basée sur les réponses de Brian Roach et 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)
    }
}
2
RamwiseMatt

C'est la même solution que @AYarulin mais supposons que le nom de la classe soit le nom de la clé JSON. De cette façon, il vous suffit de transmettre le nom de la 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);

    }
}

Ensuite, pour analyser l'échantillon de charge utile ci-dessus, nous pouvons enregistrer le désérialiseur GSON. Cela pose un problème car la clé est sensible à la casse. La casse du nom de la classe doit donc correspondre à la casse de la clé JSON. 

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

Dans mon cas, la clé "contenu" changerait pour chaque réponse. Exemple:

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

Dans de tels cas, j'ai utilisé une solution similaire à celle indiquée ci-dessus, mais je devais la modifier. Vous pouvez voir le Gist ici . C'est un peu trop gros pour le poster ici sur SOF. 

L'annotation @InnerKey("content") est utilisée et le reste du code est destiné à faciliter son utilisation avec Gson.

1
Varun Achar

Une autre solution simple:

JsonObject parsed = (JsonObject) new JsonParser().parse(jsonString);
Content content = gson.fromJson(parsed.get("content"), Content.class);
0
Vadzim

N'oubliez pas les annotations @SerializedName et @Expose pour tous les membres du groupe et les membres du groupe Inner les plus désérialisés à partir de JSON par GSON.

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

0