Jak radzić sobie z dynamicznym JSON w retroficie?

82

Używam wydajnej biblioteki sieciowej do modernizacji, ale nie mogę obsłużyć dynamicznego formatu JSON, który zawiera pojedynczy prefiks, responseMessagektóry zmienia się objectlosowo, ten sam prefiks ( responseMessage) zmienia się w niektórych przypadkach (dynamicznie).

Format Json Obiekt odpowiedzi Wiadomość:

{
   "applicationType":"1",
   "responseMessage":{
      "surname":"Jhon",
      "forename":" taylor",
      "dob":"17081990",
      "refNo":"3394909238490F",
      "result":"Received"
   }

}

responseMessage Format JSON dynamicznie zmienia się na ciąg typu:

 {
       "applicationType":"4",
       "responseMessage":"Success"          
 }

Mój problem polega na tym, że ponieważ retrofit ma wbudowane JSONparsowanie, muszę przypisać pojedyncze POJO na żądanie! ale REST-API jest niestety zbudowane na dynamicznych JSONodpowiedziach. Prefiks zmieni się losowo z ciągu na obiekt w obu metodach sukcesu (...) i niepowodzenia (...) !

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


}

W powyższym kodzie POJO TrackerRefResponse.java prefiks responseMessage jest ustawiony na string lub obiekt typu responseMessage, więc możemy stworzyć POJO ze zmienną ref o tej samej nazwie (podstawy java :)), więc szukam tego samego rozwiązania dla dynamiki JSONw Retrofit. Wiem, że jest to bardzo łatwa praca w przypadku zwykłych klientów http z zadaniem asynchronicznym, ale nie jest to najlepsza praktyka podczas JSONanalizowania REST-Api ! patrząc na wydajność Benchmarki zawsze Volley lub Retrofit to najlepszy wybór, ale nie udało mi się poradzić sobie z dynamiką JSON!

Możliwe rozwiązanie, które znam

  1. Użyj starego zadania asyc z parsowaniem klienta http. :(

  2. Spróbuj przekonać programistę backendu RESTapi.

  3. Utwórz niestandardowego klienta Retrofit :)

LOG_TAG
źródło
1
„Spróbuj przekonać programistę zaplecza RESTapi”. załatwił mi sprawę! lol! ;) (uwaga: ja też byłem programistą backendu, chciałem się przekonać!)
mrx

Odpowiedzi:

38

Spóźniony na imprezę, ale możesz użyć konwertera.

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);

Następnie używasz klasy statycznej, która implementuje Converter retrofitu:

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();
    }
}

Napisałem ten przykładowy konwerter, więc zwraca on odpowiedź Json jako String, Object, JsonObject lub Map <String, Object>. Oczywiście nie wszystkie typy zwrotów będą działać dla każdego Json i jest pewne miejsce na ulepszenia. Ale pokazuje, jak używać konwertera do konwersji prawie każdej odpowiedzi na dynamiczną Json.

Oliver Hausler
źródło
13
Widząc RestAdapterten przykład jest dla Retrofit 1. W jaki sposób można wdrożyć ten sam przetwornik w Retrofit 2?
androidtitan
1
Wyjątek
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);
}

Musisz zaimplementować metodę „checkResponseMessage”.

Yuki Yoshida
źródło
Jak mogę zrobić to samo w przypadku Retrofit 2?
Vadim Kotov
1
Co to jest „checkResponseMessage”?
Shashank singh
15

Spróbuj niestandardowej deserializacji, używając gson-converterjak poniżej (zaktualizowana odpowiedź na Retrofit 2.0)

Utwórz trzy modele, jak pokazano poniżej

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 (niestandardowy deserializator)

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

}
}

Wdrożenie Retrofit 2.0

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) {
        }
    });

Używane biblioteki

compile 'com.squareup.retrofit2: retrofit: 2.3.0'

kompiluj 'com.squareup.retrofit2: converter-gson: 2.3.0'

***** Poprzednia odpowiedź (powyższa odpowiedź jest bardziej zalecana) *****

Zmień swoje pojo w ten sposób

public class TrackerRefResponse {

  private String applicationType;
  private Object responseMessage;

  public Object getResponseMessage() {
  return responseMessage;
  }

  public void setResponseMessage(Object responseMessage) {
  this.responseMessage = responseMessage;
 }
}

i zmień w ten sposób onResponse

 @Override
public void onResponse(Response<TrackerRefResponse > response) {
    if (response.isSuccess()) {
        if (response.getResponseMessage() instanceof String)
            {
            handleStringResponse();
         }
        else 
            {
            handleObjectResponse();
         }
    }
}

możesz również sprawdzić ten post, aby uzyskać więcej informacji na temat dynamicznego analizowania json

Navneet Krishna
źródło
Czy klasa ResponseWrapper jest naprawdę potrzebna? Myślę, że wygląda to bardzo zagmatwane. Potrzebuję konwertera na czymkolwiek oprócz obiektu znajdującego się najwyżej w hierarchii ...
RabbitBones22
1
możesz uniknąć klasy opakowującej, jeśli jest ona myląca i wypróbować to freshbytelabs.com/2018/12/ ...
Navneet Krishna
9

Przyjęta odpowiedź wydawała mi się zbyt skomplikowana, rozwiązuję ją w ten sposób:

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());
    }
});

To tylko przykład, który ma pokazać, jak można używać innego modelu.

Zmienna isEmailjest po prostu wartością logiczną dla twojego warunku, aby użyć odpowiedniego modelu.

meda
źródło
Czy mógłbyś to rozwinąć? Ten kod nie ma charakteru opisowego. Skąd pochodzi mType?
Desdroid
@Desdroid Uprościłem kod, a następnie rozszerzyłem o wyjaśnienie
meda
2
Mimo to uważam, że to nie pomoże, jeśli nie znasz typu odpowiedzi przed wykonaniem połączenia, co ma miejsce w pytaniu. Oczywiście, możesz najpierw pobrać InputStream z treści odpowiedzi, przeczytać kilka wierszy, określić, jakiego typu jest treść, a następnie przekonwertować je. Ale to nie jest takie proste.
Desdroid
Szukam najlepszego sposobu obsługi różnych typów zwrotów. Twoja odpowiedź wyglądała obiecująco, ale nie byłem pewien, skąd znasz ten typ. Dlatego chciałem, żebyś to rozwinął;)
Desdroid
2
Myślę, że to nie zadziała, ponieważ Deserializer wyrzuci wyjątek i dotrze do onFailure () nie onResponse ()
Amt87
9

Każde z możliwych rozwiązań zadziała. Możesz również wysłać typ zwrotu interfejsu Retrofit api do odpowiedzi. Dzięki tej odpowiedzi otrzymasz treść, Inputstreamktórą możesz przekonwertować na obiekt JSON i odczytać według własnego uznania.

Spójrz na: http://square.github.io/retrofit/#api-declaration - w sekcji TYP OBIEKTU ODPOWIEDZI

Zaktualizowano

Retrofit 2 jest już dostępny, a wraz z nim pewne zmiany w dokumentacji i bibliotece.

Spójrz na http://square.github.io/retrofit/#restadapter-configuration, gdzie można użyć obiektu żądania i odpowiedzi.

Justin Slade
źródło
6
Obawiam się, że nie mogę znaleźć podanej przez Ciebie sekcji, czy istnieje synonim?
zionpi
7

Wiem, że jestem bardzo spóźniona na imprezę. Miałem podobny problem i po prostu rozwiązałem go w ten sposób:

public class TrackerRefResponse {

    private String applicationType;
    // Changed to Object. Works fine with String and array responses.
    private Object responseMessage;

}

Dosłownie właśnie zmieniłem tekst na Object. Wybrałem to podejście, ponieważ tylko jedno pole odpowiedzi było dynamiczne (dla mnie moja odpowiedź była o wiele bardziej skomplikowana), więc użycie konwertera utrudniłoby życie. Użył Gson do pracy z Object stamtąd, w zależności od tego, czy była to wartość String, czy Array.

Mam nadzieję, że pomoże to komuś, kto szuka prostej odpowiedzi :).

Pudełko śniadaniowe
źródło
3

Gdyby zmiana interfejsu API zaplecza nie była możliwa, rozważałbym następujące warianty (jeśli Gson jest używany do konwersji JSON).

  1. Możemy użyć adapterów typu Gson, aby stworzyć niestandardowy adapter dla ResponseMessagetypu, który dynamicznie decyduje, jak przeanalizować przychodzące JSON (używając czegoś podobnego if (reader.peek() == JsonToken.STRING)).

  2. Umieść metadane opisujące typ odpowiedzi w nagłówku HTTP i użyj ich, aby określić, jakie informacje o typie muszą zostać przekazane do instancji Gson.

Roman Mazur
źródło
1

Wiem, że się spóźniłem, ale chcę tylko podzielić się swoją myślą. Pracowałem nad projektem, w którym piszę metodę. Metoda wykorzystuje retrofit do pobierania danych z serwera. Ponieważ inni programiści w mojej firmie będą używać tej metody, nie mogłem użyć POJOklasy (w Twoim przykładzie jest to TrackerRefResponseklasa). Więc użyłem JsonObject/ Objecttak:

interfejs APIService.java

public class APIService{
    @FormUrlEncoded
    @POST
    Call<JsonObject> myPostMethod(@Url String url, @Field("input") String input);
}

Następnie w mojej metodzie napisałem to:

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
}
        

Jeśli wiesz, że określony atrybut powie Ci, jaki to typ odpowiedzi, możesz użyć JsonObject, przeczytać ten atrybut, a następnie rzucić model w następujący sposób:

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

Możesz także użyć Objectzamiast „JsonObject”. Później, kiedy będziesz wiedział, jakiego rodzaju jest to odpowiedź, być może możesz rzucić to na pożądany obiekt.

Qazi Fahim Farhan
źródło
To rozwiązanie lepiej pasuje do mojego problemu. Jedna z odpowiedzi interfejsu API z wartością null, jeśli wszystko jest w porządku, i zwraca powodzenie (kod 200) z komunikatem o błędzie w prostym obiekcie Json, gdy występuje problem. Używam JsonNull jako domyślnej odpowiedzi.
YazidEF
0

Ja też prowadziłem ten problem. ale nie jestem pewien, czy to był twój przypadek (używam Retrofit2)

w moim przypadku muszę obsłużyć komunikaty o błędach i sukcesach.

Sukces

{
"call_id": 1,
"status": "SUCCESS",
"status_code": "SUCCESS",
"result": {
    "data1": {
        "id": "RFP2UjW7p8ggpMXzYO9tRg==",
        "name": "abcdef",
        "mobile_no": "96655222",
        "email": ""
    },
    "data2": [
        {
            "no": "12345"
        },
        {
            "no": "45632"
        }
    ]
}
}

W przypadku błędu,

{
"call_id": 1,
"status": "FAILED",
"status_code": "NO_RECORDS",
"error": {
    "error_title": "xxx",
    "error_message": "details not found"
}
}

do tego właśnie stworzyłem kolejne 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;
}

w powyższym przypadku, jeśli resultklucz w json zawiera dane1, dane2, wówczas ValidateUserResult.javainicjalizacja get. jeśli błąd to Error.javainicjalizacja klasy.

Akhil T Mohan
źródło