Dedicated Server

Using custom gson converter to parse dynamic json with free source code

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

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

Now copy the classes Cars,CarModels from http://www.jsonschema2pojo.org/ and add it to your project like this

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.

2 comments:

  1. Hello,
    First 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" ..?

    ReplyDelete
    Replies
    1. 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