How to handle Dynamic JSON in Retrofit?

2019-01-01 07:15发布

问题:

I\'m using efficient networking library retrofit, but I\'m failed 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\"          
 }

The problem for me is since retrofit has built-in JSON parsing we have to assign single POJO per request! but the REST-API is unfortunately built with dynamic JSON responses, the prefix will changes to 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 :)

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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.



回答5:

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



回答6:

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



回答7:

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.



回答8:

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



回答9:

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.