Get nested JSON object with GSON using retrofit

JavaAndroidJsonGsonRetrofit

Java Problem Overview


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

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

The problem is that all my POJOs have a status, reason fields, and inside the content field is the real POJO I want.

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

Java Solutions


Solution 1 - Java

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

Let's say your JSON is:

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

You'd then have a Content POJO:

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 Gson with GsonBuilder and register the deserializer:

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

You can deserialize your JSON straight to your 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.

And finally when creating a Retrofit instance:

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

Solution 2 - Java

@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 TypeAdapter with the new instance of GSON, otherwise the second TypeAdapter will never be called. This is because we are creating a new Gson instance inside our custom deserializer.

For example, if you had the following 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:

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:

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 MyDeserializer with null values.

Solution 3 - Java

Bit late but hopefully this will help someone.

Just create following 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 :

.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

or

 yourGsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());

Solution 4 - Java

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:

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: https://stackoverflow.com/questions/34288873/retrofit-2-rxjava-gson-global-deserialization-change-response-type

Solution 5 - Java

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:

 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 = new GsonBuilder()
    .registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class, "content"))
    .build();

Solution 6 - Java

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

Solution 7 - Java

As per answer of @Brian Roach and @rafakob i done this in the following way

Json response from server

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

Common data handler class

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

    @SerializedName("code")
    public int code;

    @SerializedName("message")
    public String reason;

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

Custom serializer

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 object

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

Solution 8 - Java

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.

 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 gson = new GsonBuilder()
.registerTypeAdapter(Content.class, new RestDeserializer<>(Content.class))
.build();

Solution 9 - Java

Here's a Kotlin version based on the answers by Brian Roach and 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)
    }
}

Solution 10 - Java

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.

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

Solution 11 - Java

Don't forget @SerializedName and @Expose annotations for all Class members and Inner Class members that most deserialized from JSON by GSON.

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

Solution 12 - Java

Another simple solution:

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

Solution 13 - Java

There is a simpler way, just consider content sub object as another class:

class Content {
    var foo = 0
    var bar: String? = null
}

class Response {
    var statis: String? = null
    var reason: String? = null
    var content: Content? = null
} 

and now you can use Response type to deserialize json.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmikelarView Question on Stackoverflow
Solution 1 - JavaBrian RoachView Answer on Stackoverflow
Solution 2 - JavaKMarlowView Answer on Stackoverflow
Solution 3 - JavaMatin PetrulakView Answer on Stackoverflow
Solution 4 - JavarafakobView Answer on Stackoverflow
Solution 5 - JavaAYarulinView Answer on Stackoverflow
Solution 6 - JavaFederico J FarinaView Answer on Stackoverflow
Solution 7 - JavaRohan PawarView Answer on Stackoverflow
Solution 8 - JavaBarry MSIHView Answer on Stackoverflow
Solution 9 - JavaRamwiseMattView Answer on Stackoverflow
Solution 10 - JavaVarun AcharView Answer on Stackoverflow
Solution 11 - JavaSayed Abolfazl FatemiView Answer on Stackoverflow
Solution 12 - JavaVadzimView Answer on Stackoverflow
Solution 13 - JavaHosseinView Answer on Stackoverflow