We often come across issue where a json dynamically switches its type based on the response. For example, in normal cases suppose that you have a list of cars returned as an array from your backend, but when the number of cars in the list is one, you are given just an abject instead of array. In such cases, your code should be ready to handle the response no matter whether it is an array or an object.
To handle such cases, Gson provides plenty of customisation options. One such option is registerTypeAdapter. So lets dive deeper to check how its done.
In this tutorial we will be using retrofit to make the network calls and of course gson-converter, so go to your app level build.gradle and add the following as dependencies
please add the latest dependency versions for retrofit, converter-gson
Now check the following json strcutures that we will be parsing
cars_object_json.json
cars_array_json.json
free source code zip download
In cars_object_json.json you can see that car_models is an object because there is only one item inside it whereas in cars_array_json.json car_models is an array since there are more than one items in it. We have to write a parser which could automatically identify the response format and parse both the cases easily. registerTypeAdapter is exactly what we want here.
Alright, lets see some code now
First of all, there are two type of object models or POJOs that we need to create for the above json >> Cars,CarModels
1) Automatic model class/pojo class generation using http://www.jsonschema2pojo.org/
Now copy the classes Cars,CarModels from http://www.jsonschema2pojo.org/ and add it to your project like this
Cars.java
CarModels.java
Now, create a deserializer like shown below
In the above deserializer, you can clearly see that we have checked whether car_models is an array or an object and then parsed the response accordingly.
Now go ahead and attach the converter to your retrofit implementation so that the entire code for your MainActivity.java will look like this
And your RequestInterface.java for cars_object_json.json will be like this
RequestInterface.java for cars_array_json.json will be like this
By this method you will get the correct response for Response<Cars> response in the retrofit callbacks even if your response changes dynamically.
To handle such cases, Gson provides plenty of customisation options. One such option is registerTypeAdapter. So lets dive deeper to check how its done.
In this tutorial we will be using retrofit to make the network calls and of course gson-converter, so go to your app level build.gradle and add the following as dependencies
implementation 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
please add the latest dependency versions for retrofit, converter-gson
Now check the following json strcutures that we will be parsing
cars_object_json.json
{ "cars" : { "car_type" : "Hatchback", "car_models" : {"car_name":"Swift" , "power_window":"yes" } } }
cars_array_json.json
{ "cars" : { "car_type" : "Sedan" , "car_models" : [ { "car_name" : "Xcent", "power_window" : "no" }, { "car_name" : "Ciaz", "power_window" : "yes" } ]} }
In cars_object_json.json you can see that car_models is an object because there is only one item inside it whereas in cars_array_json.json car_models is an array since there are more than one items in it. We have to write a parser which could automatically identify the response format and parse both the cases easily. registerTypeAdapter is exactly what we want here.
Alright, lets see some code now
First of all, there are two type of object models or POJOs that we need to create for the above json >> Cars,CarModels
1) Automatic model class/pojo class generation using http://www.jsonschema2pojo.org/
- So, first of all, go to http://www.jsonschema2pojo.org/
- Paste the json https://navneet7k.github.io/cars_array_json.json in the box provided
Specify Target language as Java, Source Type as JSON and annotation style as Gson |
- Now click preview and copy the generated java files
Now copy the contents of the class and add it in android studio project |
Cars.java
public class Cars { @SerializedName("car_type") @Expose private String carType; @SerializedName("car_models") @Expose private List<CarModel> cardModels = null; public String getCarType() { return carType; } public void setCarType(String carType) { this.carType = carType; } public List<CarModel> getCardModels() { return cardModels; } public void setCardModels(List<CarModel> cardModels) { this.cardModels = cardModels; } }
CarModels.java
public class CarModel { @SerializedName("car_name") @Expose private String carName; @SerializedName("power_window") @Expose private String powerWindow; public String getCarName() { return carName; } public void setCarName(String carName) { this.carName = carName; } public String getPowerWindow() { return powerWindow; } public void setPowerWindow(String powerWindow) { this.powerWindow = powerWindow; } }
Now, create a deserializer like shown below
GsonBuilder b = new GsonBuilder(); b.registerTypeAdapter(Cars.class, new JsonDeserializer<Cars>() { @Override public Cars deserialize(JsonElement arg0, Type arg1, JsonDeserializationContext arg2) throws JsonParseException { JsonObject CarsObj = arg0.getAsJsonObject(); JsonObject innerObj=CarsObj.getAsJsonObject("cars"); Gson g = new Gson(); Cars a = g.fromJson(arg0, Cars.class); List<CarModel> carModels = null; if (innerObj.get("car_models").isJsonArray()) { carModels = g.fromJson(innerObj.get("car_models"), new TypeToken<List<CarModel>>() { }.getType()); } else { CarModel single = g.fromJson(innerObj.get("car_models"), CarModel.class); carModels = new ArrayList<CarModel>(); carModels.add(single); } a.setCardModels(carModels); return a; } });
In the above deserializer, you can clearly see that we have checked whether car_models is an array or an object and then parsed the response accordingly.
Now go ahead and attach the converter to your retrofit implementation so that the entire code for your MainActivity.java will look like this
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); parseJson(); } private void parseJson() { GsonBuilder b = new GsonBuilder(); b.registerTypeAdapter(Cars.class, new JsonDeserializer<Cars>() { @Override public Cars deserialize(JsonElement arg0, Type arg1, JsonDeserializationContext arg2) throws JsonParseException { JsonObject CarsObj = arg0.getAsJsonObject(); JsonObject innerObj=CarsObj.getAsJsonObject("cars"); Gson g = new Gson(); Cars a = g.fromJson(arg0, Cars.class); List<CarModel> carModels = null; if (innerObj.get("car_models").isJsonArray()) { carModels = g.fromJson(innerObj.get("car_models"), new TypeToken<List<CarModel>>() { }.getType()); } else { CarModel single = g.fromJson(innerObj.get("car_models"), CarModel.class); carModels = new ArrayList<CarModel>(); carModels.add(single); } a.setCardModels(carModels); return a; } }); Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://navneet7k.github.io/") .addConverterFactory(GsonConverterFactory.create(b.create())) .build(); RequestInterface request = retrofit.create(RequestInterface.class); Call<Cars> call1=request.getJson(); call1.enqueue(new Callback<Cars>() { @Override public void onResponse(Call<Cars> call, Response<Cars> response) { Toast.makeText(MainActivity.this,"Success!",Toast.LENGTH_SHORT).show(); } @Override public void onFailure(Call<Cars> call, Throwable t) { Toast.makeText(MainActivity.this,"Failure",Toast.LENGTH_SHORT).show(); } }); } }
And your RequestInterface.java for cars_object_json.json will be like this
interface RequestInterface { @GET("cars_object_json.json") Call<Cars> getJson(); }
RequestInterface.java for cars_array_json.json will be like this
interface RequestInterface { @GET("cars_array_json.json") Call<Cars> getJson(); }
By this method you will get the correct response for Response<Cars> response in the retrofit callbacks even if your response changes dynamically.
Hello,
ReplyDeleteFirst thank you very much for this post!! It helped me a lot.
Second I have a question:
How is it possible that I can call "Cars a = g.fromJson(arg0, Cars.class);" in deserialize method and that this method does not throw "IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT" ..?
Well, this statement >> Cars a = g.fromJson(arg0, Cars.class); here has no use here, i might have added it by mistake. The reason it doesn't throw "Expected BEGIN_ARRAY but was BEGIN_OBJECT" is that the json contains an object as expected from our code, but the json has different field names as compared to our code("cars"), since gson cant find this variable, it just sets the attributes as null at first and later fills in the list. You may just replace this line >> Cars a = g.fromJson(arg0, Cars.class); with this one >> Cars a =new Cars(); . This will correctly fill your List with cars
Delete