How to handle Dynamic JSON in Retrofit?
AndroidJsonGsonRetrofitAndroid Problem Overview
I am using the retrofit efficient networking library, but I am unable to handle Dynamic JSON which contains single prefix responseMessage
which changes to object
randomly, the same prefix ( responseMessage
) changes to String in some cases (dynamically).
Json format Object of responseMessage:
{
"applicationType":"1",
"responseMessage":{
"surname":"Jhon",
"forename":" taylor",
"dob":"17081990",
"refNo":"3394909238490F",
"result":"Received"
}
}
responseMessage
Json format dynamically changes to type string:
{
"applicationType":"4",
"responseMessage":"Success"
}
My problem is since retrofit has built-in JSON
parsing, I have to assign single POJO per request! but the REST-API unfortunately, is built on dynamic JSON
responses. The prefix will change from string to object randomly in both success(...) and failure(...) methods!
void doTrackRef(Map<String, String> paramsref2) {
RestAdapter restAdapter = new RestAdapter.Builder().setEndpoint("http://192.168.100.44/RestDemo").build();
TrackerRefRequest userref = restAdapter.create(TrackerRefRequest.class);
userref.login(paramsref2,
new Callback<TrackerRefResponse>() {
@Override
public void success(
TrackerRefResponse trackdetailresponse,
Response response) {
Toast.makeText(TrackerActivity.this, "Success",
Toast.LENGTH_SHORT).show();
}
@Override
public void failure(RetrofitError retrofitError) {
Toast.makeText(TrackerActivity.this, "No internet",
Toast.LENGTH_SHORT).show();
}
});
}
Pojo:
public class TrackerRefResponse {
private String applicationType;
private String responseMessage; //String type
//private ResponseMessage responseMessage; //Object of type ResponseMessage
//Setters and Getters
}
In above code POJO TrackerRefResponse.java prefix responseMessage is set to string or object of type responseMessage , so we can create the POJO with ref variable with same name (java basics :) ) so I'm looking for same solution for dynamic JSON
in Retrofit.
I know this is very easy job in normal http clients with async task, but it's not the best practice in the REST-Api JSON
parsing! looking at the performance Benchmarks always Volley or Retrofit is the best choice, but I'm failed handle dynamic JSON
!
Possible solution I Know
-
Use old asyc task with http client parsing. :(
-
Try to convince the RESTapi backend developer.
-
Create custom Retrofit client :)
Android Solutions
Solution 1 - Android
Late to the party, but you can use a converter.
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://graph.facebook.com")
.setConverter(new DynamicJsonConverter()) // set your static class as converter here
.build();
api = restAdapter.create(FacebookApi.class);
Then you use a static class which implements retrofit's Converter:
static class DynamicJsonConverter implements Converter {
@Override public Object fromBody(TypedInput typedInput, Type type) throws ConversionException {
try {
InputStream in = typedInput.in(); // convert the typedInput to String
String string = fromStream(in);
in.close(); // we are responsible to close the InputStream after use
if (String.class.equals(type)) {
return string;
} else {
return new Gson().fromJson(string, type); // convert to the supplied type, typically Object, JsonObject or Map<String, Object>
}
} catch (Exception e) { // a lot may happen here, whatever happens
throw new ConversionException(e); // wrap it into ConversionException so retrofit can process it
}
}
@Override public TypedOutput toBody(Object object) { // not required
return null;
}
private static String fromStream(InputStream in) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder out = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
out.append(line);
out.append("\r\n");
}
return out.toString();
}
}
I have written this sample converter so it returns the Json response either as String, Object, JsonObject or Map< String, Object >. Obviously not all return types will work for every Json, and there is sure room for improvement. But it demonstrates how to use a Converter to convert almost any response to dynamic Json.
Solution 2 - Android
RestClient.java
import retrofit.client.Response;
public interface RestClient {
@GET("/api/foo") Response getYourJson();
}
YourClass.java
RestClient restClient;
// create your restClient
Response response = restClient.getYourJson();
Gson gson = new Gson();
String json = response.getBody().toString();
if (checkResponseMessage(json)) {
Pojo1 pojo1 = gson.fromJson(json, Pojo1.class);
} else {
Pojo2 pojo2 = gson.fromJson(json, Pojo2.class);
}
You must implement "checkResponseMessage" method.
Solution 3 - Android
Try custom deserialisation using gson-converter
as below(updated answer for Retrofit 2.0)
Create three models as shown below
ResponseWrapper
public class ResponseWrapper {
@SerializedName("applicationType")
@Expose
private String applicationType;
@SerializedName("responseMessage")
@Expose
private Object responseMessage;
public String getApplicationType() {
return applicationType;
}
public void setApplicationType(String applicationType) {
this.applicationType = applicationType;
}
public Object getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(Object responseMessage) {
this.responseMessage = responseMessage;
}
}
ResponseMessage
public class ResponseMessage extends ResponseWrapper {
@SerializedName("surname")
@Expose
private String surname;
@SerializedName("forename")
@Expose
private String forename;
@SerializedName("dob")
@Expose
private String dob;
@SerializedName("refNo")
@Expose
private String refNo;
@SerializedName("result")
@Expose
private String result;
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
public String getForename() {
return forename;
}
public void setForename(String forename) {
this.forename = forename;
}
public String getDob() {
return dob;
}
public void setDob(String dob) {
this.dob = dob;
}
public String getRefNo() {
return refNo;
}
public void setRefNo(String refNo) {
this.refNo = refNo;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
ResponseString
public class ResponseString extends ResponseWrapper {
}
UserResponseDeserializer(custom deserialiser)
public class UserResponseDeserializer implements JsonDeserializer<ResponseWrapper> {
@Override
public ResponseWrapper deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (((JsonObject) json).get("responseMessage") instanceof JsonObject){
return new Gson().fromJson(json, ResponseMessage.class);
} else {
return new Gson().fromJson(json, ResponseString.class);
}
}
}
Retrofit 2.0 Implementation
Gson userDeserializer = new GsonBuilder().setLenient().registerTypeAdapter(ResponseWrapper.class, new UserResponseDeserializer()).create();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("base_url")
.addConverterFactory(GsonConverterFactory.create(userDeserializer))
.build();
UserService request = retrofit.create(UserService.class);
Call<ResponseWrapper> call1=request.listAllUsers();
call1.enqueue(new Callback<ResponseWrapper>() {
@Override
public void onResponse(Call<ResponseWrapper> call, Response<ResponseWrapper> response) {
ResponseWrapper responseWrapper=response.body();
Log.i("DYNAMIC RESPONSE", String.valueOf(response.body().getResponseMessage()));
}
@Override
public void onFailure(Call<ResponseWrapper> call, Throwable t) {
}
});
Libraries Used
> compile 'com.squareup.retrofit2:retrofit:2.3.0'
> compile 'com.squareup.retrofit2:converter-gson:2.3.0'
***** Previous Answer (above answer is more recommended one) *****
Change your pojo like this
public class TrackerRefResponse {
private String applicationType;
private Object responseMessage;
public Object getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(Object responseMessage) {
this.responseMessage = responseMessage;
}
}
and change retrofit's onResponse like this
@Override
public void onResponse(Response<TrackerRefResponse > response) {
if (response.isSuccess()) {
if (response.getResponseMessage() instanceof String)
{
handleStringResponse();
}
else
{
handleObjectResponse();
}
}
}
you may also check this post for more details about dynamic json parsing
Solution 4 - Android
Any of your possible solutions will work. What you can also do is send the Retrofit api interface return type to response. With that response you get a body Inputstream
which you can convert to a JSON Object and read as you see fit.
Look at: http://square.github.io/retrofit/#api-declaration - under RESPONSE OBJECT TYPE
Updated
Retrofit 2 is out now and with it some changes to the documentation and library.
Look at http://square.github.io/retrofit/#restadapter-configuration there are request and response body object that can be used.
Solution 5 - Android
The accepted answer seemed over complicated for me, I solve it this way:
Call<ResponseBody> call = client.request(params);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Response<ResponseBody> response) {
if (response.isSuccess()) {
Gson gson = new Gson();
ResponseBody repsonseBody = response.body().string();
if (isEmail) {
EmailReport reports = gson.fromJson(responseBody, EmailReport.class);
} else{
PhoneReport reports = gson.fromJson(repsonseBody, PhoneReport.class);
}
}
}
@Override
public void onFailure(Throwable t) {
Log.e(LOG_TAG, "message =" + t.getMessage());
}
});
This is just an example in attempt to show you how you can use different model.
The variable isEmail
is just a boolean for your condition to use the appropriate model.
Solution 6 - Android
I know I am very very late to the party. I had a similar issue and just solved it like this:
public class TrackerRefResponse {
private String applicationType;
// Changed to Object. Works fine with String and array responses.
private Object responseMessage;
}
I literally just changed to type to Object. I chose this approach because only one field in the response was dynamic (for me, my response was way more complicated), so using a converter would have made life difficult. Used Gson to work with the Object from there, depending on if it was a String or Array value.
Hope this helps someone looking for a simple answer :).
Solution 7 - Android
If it was not possible to change the backend API, I would consider the following variants (if Gson is used to convert JSON).
-
We can use Gson type adapters to create a custom adapter for
ResponseMessage
type that dynamically decides how to parse the inoming JSON (using something likeif (reader.peek() == JsonToken.STRING)
). -
Put some meta information describing the response type into an HTTP header and use it to determine what type information must be fed to Gson instance.
Solution 8 - Android
For kotlin developers you can declare the type of your variable as Any then convert it to Gson class with gson converter
data class SomeClass(
...somevariables,
val respnseMessage : Any
)
Then in your activity or fragment wherever you want to parse and use it. based on your use case you can do something like this
val gson = Gson()
val data = gson.fromJson<YourDesireClass>
(response.responseMessage.toString() , YourDesireClass::class.java)
Solution 9 - Android
In addition to what you told -
Use Callback
http://google-gson.googlecode.com/svn/tags/1.2.3/docs/javadocs/com/google/gson/JsonObject.html
Solution 10 - Android
I know I am late, but I just want to share my thought. I was working on a project where I am writing a method. The method uses retrofit to get data from server. Since other developers in my company will use this method, I could not use a POJO
class (in your example, the TrackerRefResponse
class). So I used JsonObject
/ Object
like this:
interface APIService.java
public class APIService{
@FormUrlEncoded
@POST
Call<JsonObject> myPostMethod(@Url String url, @Field("input") String input);
}
Then in my method, I wrote this:
Model1 model1 = null;
Model2 model2 = null;
Call<JsonObject> call = RetrofitClient.getAPIService().establishUserSession(post_request_url, someParameter);
call.enqueue(new Callback<JsonObject>() {
@Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> response) {
JsonObject jsonObject = response.body();
// then do your stuff. maybe something like this
try{
model1 = new Gson().fromJson(jsonObject, Model1.class);
}catch(Exception x){}
try{
model2 = new Gson().fromJson(jsonObject, Model2.class);
}catch(Exception x){}
if(model1 != null) { /*handle model1 */}
if(model2 != null) { /*handle model2*/}
// rest of the code
}
If you know that a certain attibute will tell you which type of response it is, you can use JsonObject , read that attribute and then cast the model like this way:
// ... retrofit codes
@Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> response) {
int number = jsonObject.get("applicationType").getAsInt();
if(number == 1) {
model1 = new Gson().fromJson(jsonObject, Model1.class);
}
}
// ... rest of the code
You can also use Object
instead of 'JsonObject`. Later, when you will know which kind of response it is, maybe you can cast this into desired object.
Solution 11 - Android
I too ran of this issue. but i am not sure if this was your case , (i am using Retrofit2)
on my case i need to handle error, and success messages.
On Success
{
"call_id": 1,
"status": "SUCCESS",
"status_code": "SUCCESS",
"result": {
"data1": {
"id": "RFP2UjW7p8ggpMXzYO9tRg==",
"name": "abcdef",
"mobile_no": "96655222",
"email": ""
},
"data2": [
{
"no": "12345"
},
{
"no": "45632"
}
]
}
}
On Error,
{
"call_id": 1,
"status": "FAILED",
"status_code": "NO_RECORDS",
"error": {
"error_title": "xxx",
"error_message": "details not found"
}
}
for this i just created another POJO Error
,
public class ValidateUserResponse {
@SerializedName("call_id")
public String callId;
@SerializedName("status")
public String status;
@SerializedName("status_code")
public String statusCode;
@SerializedName("result")
public ValidateUserResult result;
@SerializedName("error")
public Error error;
}
Error.java
public class Error {
@SerializedName("error_title")
public String errorTitle;
@SerializedName("error_message")
public String errorMessage;
}
ValidateUser.java
public class ValidateUserResult {
@SerializedName("auth_check")
public String authCheck;
@SerializedName("data1")
public Data1 data1;
@SerializedName("data2")
public List<Data2> data2;
}
in the above case if the result
key on json contains data1,data2 then the ValidateUserResult.java
get initialised.
if error then the Error.java
class get initialized.
Solution 12 - Android
Just look into the other options it works for me:-
Json
1.
{
"applicationType":"1",
"responseMessage":
{
"surname":"Jhon",
"forename":" taylor",
"dob":"17081990",
"refNo":"3394909238490F",
"result":"Received"
}
}
2.
{
"applicationType":"4",
"responseMessage":
{
"forename":" taylor",
"dob":"17081990",
}
}
3.
{
"applicationType":"5",
"responseMessage":
{
"refNo":"3394909238490F",
"result":"Received"
}
}
Pojo class will be:-
public class ResponseMessage
{
private String surname;
private String forename;
private String dob;
private String refNo;
private String result;
public void setSurname(String surname){
this.surname = surname;
}
public String getSurname(){
return this.surname;
}
public void setForename(String forename){
this.forename = forename;
}
public String getForename(){
return this.forename;
}
public void setDob(String dob){
this.dob = dob;
}
public String getDob(){
return this.dob;
}
public void setRefNo(String refNo){
this.refNo = refNo;
}
public String getRefNo(){
return this.refNo;
}
public void setResult(String result){
this.result = result;
}
public String getResult(){
return this.result;
}
}
public class Root
{
private String applicationType;
private ResponseMessage responseMessage;
public void setApplicationType(String applicationType){
this.applicationType = applicationType;
}
public String getApplicationType(){
return this.applicationType;
}
public void setResponseMessage(ResponseMessage responseMessage){
this.responseMessage = responseMessage;
}
public ResponseMessage getResponseMessage(){
return this.responseMessage;
}
}
now final code
if(responseMessage.getSurname() !=null){
---do something---
}
if(responseMessage.getForename !=null){
----do something
}
if(responseMessage.getDob() !=null){
---do something---
}
if(responseMessage.getRefNo() !=null){
---do something---
}
if(responseMessage.getResult() !=null){
---do something---
}