Obtenez un objet JSON imbriqué avec GSON à l'aide de la modernisation

111

Je consomme une API de mon application Android, et toutes les réponses JSON sont comme ceci:

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

Le problème est que tous mes POJO ont un status, les reasonchamps, et à l' intérieur du contentchamp est le vrai POJO je veux.

Existe-t-il un moyen de créer un convertisseur personnalisé de Gson pour extraire toujours le contentchamp, de sorte que la mise à niveau renvoie le POJO approprié?

Mikelar
la source
J'ai lu le document mais je ne vois pas comment faire ...: (Je ne sais pas comment programmer le code pour résoudre mon problème
mikelar
Je suis curieux de savoir pourquoi vous ne formateriez pas simplement votre classe POJO pour gérer ces résultats d'état.
jj.

Réponses:

168

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

Disons que votre JSON est:

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

Vous auriez alors un ContentPOJO:

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 un Gsonwith GsonBuilderet 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 à partir des commentaires:

Si vous avez différents types de messages mais qu'ils ont tous le champ «contenu», vous pouvez rendre le désérialiseur générique en faisant:

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

    }
}

Il vous suffit d'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 transporté 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();
Brian Roach
la source
1
Wow c'est génial! Merci! : D Existe-t-il un moyen de généraliser la solution pour ne pas avoir à créer un JsonDeserializer pour chaque type de réponse?
mikelar le
1
Ceci est incroyable! Une chose à changer: Gson gson = new GsonBuilder (). Create (); Au lieu de Gson gson = new GsonBuilder (). Build (); Il y a deux exemples de cela.
Nelson Osacky
7
@feresr vous pouvez appeler setConverter(new GsonConverter(gson))dans la RestAdapter.Builderclasse Retrofit
akhy
2
@BrianRoach merci, belle réponse .. devrais-je m'enregistrer Person.classet List<Person>.class/ Person[].classavec désérialiseur séparé?
akhy
2
Une possibilité d'obtenir le «statut» et la «raison» aussi? Par exemple, si toutes les requêtes les renvoient, pouvons-nous les avoir dans une super classe et utiliser des sous-classes qui sont les POJO réels de "content"?
Nima G du
14

La solution de @ BrianRoach est la bonne solution. Il est à noter que dans le cas particulier où vous avez des objets personnalisés imbriqués qui ont tous deux besoin d'une personnalisation TypeAdapter, vous devez enregistrer le TypeAdapteravec la nouvelle instance de GSON , sinon la seconde TypeAdapterne sera jamais appelée. C'est parce que nous créons une nouvelle Gsoninstance 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 mappé aux objets suivants:

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

class SubContent
{
    public String baz;
}

Vous devrez enregistrer les SubContentfichiers TypeAdapter. Pour être plus robuste, vous pouvez effectuer les opérations suivantes:

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é en passant simplement une nouvelle instance de MyDeserializeravec des valeurs nulles.

KMarlow
la source
1
De quel package est "Type"? Il existe un million de packages contenant la classe "Type". Je vous remercie.
Kyle Bridenstine
2
@ Mr.Tea It'll bejava.lang.reflect.Type
aidan
1
Où est la classe SubContentDeserializer? @KMarlow
LogronJ
10

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

Créez simplement TypeAdapterFactory suivant.

    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 ajoutez-le dans votre générateur GSON:

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

ou

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Matin Petrulak
la source
C'est exactement ce que je regarde. Parce que j'ai beaucoup de types enveloppés avec un nœud "data" et que je ne peux pas ajouter TypeAdapter à chacun d'eux. Merci!
Sergey Irisov
@SergeyIrisov vous êtes les bienvenus. Vous pouvez voter pour cette réponse pour qu'elle augmente :)
Matin Petrulak
Comment passer plusieurs jsonElement?. comme je l'ai content, content1etc.
Sathish Kumar J
Je pense que vos développeurs back-end devraient changer la structure et ne pas passer de contenu, content1 ... Quel est l'avantage de cette approche?
Matin Petrulak
Je vous remercie! C'est la réponse parfaite. @Marin Petrulak: L'avantage est que ce formulaire est à l'épreuve du temps pour les changements. «contenu» est le contenu de la réponse. Dans le futur, ils peuvent venir de nouveaux champs comme "version", "lastUpdated", "sessionToken" et ainsi de suite. Si vous n'avez pas encapsulé votre contenu de réponse au préalable, vous rencontrerez un certain nombre de solutions de contournement dans votre code pour vous adapter à la nouvelle structure.
muetzenflo
7

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

Emballage:

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

Exception personnalisée à lever, 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 de type de réponse

Rafakob
la source
À quoi ressemble MyPojo?
IgorGanapolsky
1
@IgorGanapolsky MyPojo peut ressembler à ce que vous voulez. Il doit correspondre à vos données de contenu récupérées sur un serveur. La structure de cette classe doit être adaptée à votre convertisseur de sérialisation (Gson, Jackson, etc.).
rafakob
@rafakob pouvez-vous m'aider avec mon problème également? J'ai du mal à essayer d'obtenir un champ dans mon json imbriqué de la manière la plus simple possible. voici ma question: stackoverflow.com/questions/56501897/…
6

Poursuivant l'idée de Brian, car nous avons presque toujours de nombreuses ressources REST, chacune avec 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 l'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();
AYarulin
la source
3

Une meilleure solution pourrait être celle-ci.

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

Ensuite, définissez votre service comme ceci.

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

Selon la réponse de @Brian Roach et @rafakob, je l'ai fait 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());
                    }
                });
Rohan Pawar
la source
2

C'est la même solution que @AYarulin mais supposons que le nom de classe est 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'exemple de charge utile ci-dessus, nous pouvons enregistrer le désérialiseur GSON. Ceci est problématique car la clé est sensible à la casse, donc la casse du nom de classe doit correspondre à la casse de la clé JSON.

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

Voici une version 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)
    }
}
RamwiseMatt
la source
1

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 j'ai dû la modifier. Vous pouvez voir l'essentiel ici . C'est un peu trop grand pour le publier ici sur SOF.

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

Varun Achar
la source
Pouvez-vous également répondre à ma question. Passez du temps à récupérer un champ à partir d'un json imbriqué de la manière la plus simple possible: stackoverflow.com/questions/56501897
0

Une autre solution simple:

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