Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get nested JSON object with GSON using retrofit

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?

like image 738
mikelar Avatar asked Apr 14 '14 20:04

mikelar


People also ask

What does Gson toJson do?

Introduction. Gson is the main actor class of Google Gson library. It provides functionalities to convert Java objects to matching JSON constructs and vice versa. Gson is first constructed using GsonBuilder and then toJson(Object) or fromJson(String, Class) methods are used to read/write JSON constructs.


2 Answers

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(); 
like image 198
Brian Roach Avatar answered Sep 29 '22 23:09

Brian Roach


@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.

like image 23
KMarlow Avatar answered Sep 29 '22 22:09

KMarlow