89

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

  1. Use old asyc task with http client parsing. :(

  2. Try to convince the RESTapi backend developer.

  3. Create custom Retrofit client :)

David Kariuki
  • 1,522
  • 1
  • 15
  • 30
LOG_TAG
  • 19,894
  • 12
  • 72
  • 105
  • 1
    "Try to convince the RESTapi backend developer." did the trick for me! lol! ;) (n.b: I was the backend dev too, me to convince myself!) – mrx May 06 '20 at 07:01

12 Answers12

38

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.

Ziem
  • 6,579
  • 8
  • 53
  • 86
Oliver Hausler
  • 4,900
  • 4
  • 35
  • 70
20

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.

Yuki Yoshida
  • 1,233
  • 1
  • 15
  • 28
18

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

Navneet Krishna
  • 5,009
  • 5
  • 25
  • 44
  • Is the ResponseWrapper class truly necessary? I think it looks very confusing. I need a converter on anything but the object highest in the hierarchy... – RabbitBones22 Jan 09 '19 at 13:22
  • 1
    you may avoid the wrapper class if its confusing and try this https://www.freshbytelabs.com/2018/12/using-custom-gson-converter-to-parse.html – Navneet Krishna Jan 09 '19 at 13:41
10

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.

Justin Slade
  • 500
  • 2
  • 7
9

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.

meda
  • 45,103
  • 14
  • 92
  • 122
  • Could you please elaborate that? This code is non descriptive. Where does mType come from? – Desdroid Feb 26 '16 at 18:14
  • @Desdroid I simplified the code and then expanded with explanation – meda Feb 26 '16 at 18:28
  • 2
    Still, I believe this won't help if you don't know the response Type before doing the call, which is the case in the question. Sure you could first get the InputStream of the Response Body, read a few lines, determine of which Type the Body is and then convert it. But it's just not as simple as that. – Desdroid Feb 26 '16 at 18:54
  • I am looking for the best way to go in handling different return types. Your answer looked pretty promising, but I wasn't sure where you knew the Type from. That's why I wanted you to elaborate that ;) – Desdroid Feb 26 '16 at 20:59
  • 2
    I think it will not work because Deserializer will throw an exception and reach the onFailure() not onResponse() – Amt87 Aug 10 '16 at 07:51
8

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 :).

Lunchbox
  • 1,538
  • 2
  • 16
  • 42
3

If it was not possible to change the backend API, I would consider the following variants (if Gson is used to convert JSON).

  1. 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 like if (reader.peek() == JsonToken.STRING)).

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

Roman Mazur
  • 3,084
  • 1
  • 20
  • 24
3

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)
mmdreza baqalpour
  • 1,106
  • 9
  • 18
1

In addition to what you told -

Use Callback Then you can retrieve the fields using regular get method. For more information, go through the javadoc of gson.

http://google-gson.googlecode.com/svn/tags/1.2.3/docs/javadocs/com/google/gson/JsonObject.html

Sugesh
  • 246
  • 2
  • 6
1

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.

Qazi Fahim Farhan
  • 2,066
  • 1
  • 14
  • 26
  • This solution fit my problem better. One of api response with null if everything is ok, and return success (code 200) with error message in simple Json object when there is issue. I use JsonNull as default response. – Yazid Nov 26 '20 at 03:24
0

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.

Akhil T Mohan
  • 193
  • 1
  • 1
  • 14
0

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