web-dev-qa-db-fra.com

Retrofit2 Gère la condition lorsque le code d'état 200, mais la structure JSON est différente de celle de la classe datamodel

J'utilise Retrofit2 et RxJava2CallAdapterFactory.

L'API que je consomme renvoie toujours le code d'état 200 et, pour la chaîne JSON de succès et de réponse, la structure JSON est entièrement différente. Le code de statut étant toujours 200, la méthode onResponse () est toujours appelée. Par conséquent, je ne suis pas en mesure d'extraire les messages d'erreur du JSON dans la condition d'erreur.

Solution 1:

J'utilise ScalarsConverterFactory pour obtenir une chaîne de réponse et j'utilise manuellement Gson pour analyser la réponse. Comment obtenir une réponse sous forme de chaîne à l'aide de la modification sans utiliser GSON ou toute autre bibliothèque dans Android

Problème avec cette solution : J'envisage d'utiliser RxJava2CallAdapterFactory pour que la méthode de conversion retourne DataModel Class.

Je dois trouver la meilleure solution à ce problème, de manière à pouvoir renvoyer les classes de modèle de données de la méthode Retrofit et en quelque sorte, j'identifie la condition d'erreur à partir de la réponse (identifiez la réponse JSON ne correspond pas au modèle de données), puis analysez l'erreur JSON dans un modèle de données.

Client en rénovation

 public static Retrofit getClient(String url) {
        if (apiClient == null) {

            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();

            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(interceptor).build();
            apiClient = new Retrofit.Builder()
                    .baseUrl(url)
                    /*addCallAdapterFactory for RX Recyclerviews*/
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    /* add ScalarsConverterFactory to get json string as response */
//                    .addConverterFactory(ScalarsConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
//                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .client(httpClient)
                    .build();
        }
        return apiClient;
    }

Méthode

public static void getLoginAPIResponse(String username, String password, String sourceId, String uuid, final HttpCallback httpCallback) {
        baseUrl = AppPreference.getParam(UiUtils.getContext(), SPConstants.BASE_URL, "").toString();
        ApiInterface apiService =
                ApiClient.getClient(baseUrl).create(ApiInterface.class);

        Call<LoginBean> call = apiService.getLoginResponse(queryParams);
        call.enqueue(new Callback<LoginBean>() {

            @Override
            public void onResponse(Call<LoginBean> call, Response<LoginBean> response) {

                if (response.body().isObjectNull()) {
                    httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                            HttpCallback.RETURN_TYPE_FAILURE, 0, null);
                    return;
                }
                httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_SUCCESS, response.code(), response.body());
            }





        @Override
        public void onFailure(Call<LoginBean> call, Throwable t) {
            // Log error here since request failed
            httpCallback.resultCallback(APIConstants.API_APP_VERIFICATION, HttpCallback.REQUEST_TYPE_GET,
                    HttpCallback.RETURN_TYPE_FAILURE, 0, t);
            t.printStackTrace();
        }
    });
}

Interface

@GET("App/login")
Call<LoginBean> getLoginResponse(@QueryMap Map<String, String> queryMap);

PS: L’API ne peut pas changer pour le moment, car d’autres applications le consomment.

  • L'analyseur Gson ne renvoie pas d'instance d'objet null pour que je comprenne qu'il existe une incompatibilité de structure Json et de modèle de données.

  • RestAdapter est obsolète dans Retrofit 2 

Je cherche la meilleure solution pour résoudre ce problème, évitant de préférence l’analyse manuelle json et tirant le meilleur parti des adaptations Rénovation et ultérieures.

EDIT 

Code de réponse 200 donc 

  1. response.isSuccessful() == true 

  2. response.body() != null est également vrai, car Gson ne crée jamais d'instance nulle ni ne lève d'exception en cas d'incompatibilité de structure JSON. 

  3. response.errorBody() == null à tout moment en tant que réponse envoyée en tant que flux d'entrée à partir du serveur.

    if (response.isSuccessful() && response.body() != null) {
        //control always here as status code 200 for error condition also
    }else if(response.errorBody()!=null){
        //control never reaches here
    }
    

EDIT 2

SOLUTION

La solution est basée sur une réponse anstaendig 

  • J'ai créé une classe générique de base pour approfondir cette réponse. 
  • Depuis que j'ai plusieurs apis et modèles de données, je dois créer des désérilisants pour chaque 

BASE API BEAN  

public class BaseApiBean<T> {

    @Nullable
    private T responseBean;

    @Nullable
    private ErrorBean errorBean;

    public BaseApiBean(T responseBean, ErrorBean errorBean) {
        this.responseBean = responseBean;
        this.errorBean = errorBean;
    }

    public T getResponseBean() {
        return responseBean;
    }

    public void setResponseBean(T responseBean) {
        this.responseBean = responseBean;
    }

    public ErrorBean getErrorBean() {
        return errorBean;
    }

    public void setErrorBean(ErrorBean errorBean) {
        this.errorBean = errorBean;
    }
}

BASE DESERIALIZER  

  public abstract class BaseDeserializer implements JsonDeserializer<BaseApiBean> {


        @Override
        public BaseApiBean deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                throws JsonParseException {
            // Get JsonObject
            final JsonObject jsonObject = json.getAsJsonObject();
            if (jsonObject.has("result")) {
                   /* {"result":"404"}*/
                ErrorBean errorMessage = new Gson().fromJson(jsonObject, ErrorBean.class);
                return getResponseBean(errorMessage);
            } else {

                return getResponseBean(jsonObject);
            }
        }

        public abstract BaseApiBean getResponseBean(ErrorBean errorBean);
        public abstract BaseApiBean getResponseBean(JsonObject jsonObject);
    }

Désérialiseur personnalisé pour chaque API  

public class LoginDeserializer extends BaseDeserializer {


    @Override
    public BaseApiBean getResponseBean(ErrorBean errorBean) {
        return new LoginResponse(null, errorBean);
    }

    @Override
    public BaseApiBean getResponseBean(JsonObject jsonObject) {

        LoginBean loginBean = (new Gson().fromJson(jsonObject, LoginBean.class));
        return new LoginResponse(loginBean, null);
    }
}

BEAN DE RÉPONSE PERSONNALISÉE

public class LoginResponse extends BaseApiBean<LoginBean> {
    public LoginResponse(LoginBean responseBean, ErrorBean errorBean) {
        super(responseBean, errorBean);
    }

}

CLIENT

public class ApiClient {
    private static Retrofit apiClient = null;
    private static Retrofit apiClientForFeedBack = null;
    private static LoginDeserializer loginDeserializer = new LoginDeserializer();
    private static AppVerificationDeserializer appVerificationDeserializer = new AppVerificationDeserializer();


    public static Retrofit getClient(String url) {
        if (apiClient == null) {

            GsonBuilder gsonBuilder=new GsonBuilder();
            gsonBuilder.registerTypeAdapter(LoginResponse.class,
                    loginDeserializer);
            gsonBuilder.registerTypeAdapter(AppVerificationResponse.class,
                    appVerificationDeserializer);
            Gson gson= gsonBuilder.create();


            HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();

            interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            OkHttpClient httpClient = new OkHttpClient.Builder().addInterceptor(interceptor)
                    .retryOnConnectionFailure(true)
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .build();

            apiClient = new Retrofit.Builder()
                    .baseUrl(url)
                    /*addCallAdapterFactory for RX Recyclerviews*/
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    /* add ScalarsConverterFactory to get json string as response */
//                    .addConverterFactory(ScalarsConverterFactory.create())
//                    .addConverterFactory(GsonConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .client(httpClient)
                    .build();
        }
        return apiClient;
    }

RÉPONSE DE LA POIGNÉE  

 public static void getLoginAPIResponse(String username, String password, String sourceId, String uuid, final HttpCallback httpCallback) {
        baseUrl = AppPreference.getParam(getContext(), SPConstants.MT4_BASE_URL, "").toString();
        ApiInterface apiService =
                ApiClient.getClient(baseUrl).create(ApiInterface.class);
        HashMap<String, String> queryParams = new HashMap<>();
        queryParams.put(APIConstants.KEY_EMAIL, sourceId + username.toLowerCase());
        queryParams.put(APIConstants.KEY_PASSWORD, Utils.encodePwd(password));


        Call<LoginResponse> call = apiService.getLoginResponse(queryParams);
        call.enqueue(new Callback<LoginResponse>() {

            @Override
            public void onResponse(Call<LoginResponse> call, Response<LoginResponse> response) {

                if (response.body().getResponseBean()==null) {
                    httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                            HttpCallback.RETURN_TYPE_FAILURE, 0,  response.body().getErrorBean());
                    return;
                }
                httpCallback.resultCallback(APIConstants.API_LOGIN, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_SUCCESS, response.code(), response.body().getResponseBean());

            }


            @Override
            public void onFailure(Call<LoginResponse> call, Throwable t) {
                // Log error here since request failed
                httpCallback.resultCallback(APIConstants.API_APP_VERIFICATION, HttpCallback.REQUEST_TYPE_GET,
                        HttpCallback.RETURN_TYPE_FAILURE, 0, t);
                t.printStackTrace();
            }
        });
    }
15
Rachita Nanda

Vous avez donc deux réponses différentes réussies (code d'état 200) du même point de terminaison. L'une étant le modèle de données réel et l'autre une erreur (à la fois comme une structure json comme celle-ci ?:

Réponse LoginBean valide:

{
  "id": 1234,
  "something": "something"
}

Réponse d'erreur

{
  "error": "error message"
}

Ce que vous pouvez faire, c'est avoir une entité qui enveloppe les deux cas et utiliser un désérialiseur personnalisé.

class LoginBeanResponse {
  @Nullable private final LoginBean loginBean;
  @Nullable private final ErrorMessage errorMessage;

  LoginBeanResponse(@Nullable LoginBean loginBean, @Nullable ErrorMessage errorMessage) {
    this.loginBean = loginBean;
    this.errorMessage = errorMessage;
  }
  // Add getters and whatever you need
}

Un wrapper pour l'erreur:

class ErrorMessage {
  String errorMessage;
  // And whatever else you need
  // ...
}

Alors vous avez besoin d'une JsonDeserializer:

public class LoginBeanResponseDeserializer implements JsonDeserializer<LoginBeanResponse> {

  @Override
  public LoginBeanResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

    // Based on the structure you check if the data is valid or not
    // Example for the above defined structures:

    // Get JsonObject
    final JsonObject jsonObject = json.getAsJsonObject();
    if (jsonObject.has("error") {
      ErrorMessage errorMessage = new Gson().fromJson(jsonObject, ErrorMessage.class);
      return new LoginBeanResponse(null, errorMessage)
    } else {
      LoginBean loginBean = new Gson().fromJson(jsonObject, LoginBean.class):
      return new LoginBeanResponse(loginBean, null);
    }
  }
}

Ajoutez ensuite ce désérialiseur à la GsonConverterFactory:

GsonBuilder gsonBuilder = new GsonBuilder().registerTypeAdapter(LoginBeanResponse.class, new LoginBeanResponseDeserializer()).create():

apiClient = new Retrofit.Builder()
    .baseUrl(url)
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .addConverterFactory(GsonConverterFactory.create(gsonBuilder))
    .client(httpClient)
    .build();

C’est la seule façon dont je peux penser à faire ce travail. Mais comme déjà mentionné, ce type de conception d'API est tout simplement faux, car les codes d'état sont là pour une raison. J'espère toujours que cela aide.

EDIT: Ce que vous pouvez ensuite faire dans la classe où vous effectuez l’appel de cette conversion (si vous avez déjà converti de Call<LoginBeanResponse> à Single<LoginBeanResponse> avec RxJava) renvoie en réalité une erreur correcte. Quelque chose comme:

Single<LoginBean> getLoginResponse(Map<String, String> queryMap) {
    restApi.getLoginResponse(queryMap)
        .map(loginBeanResponse -> { if(loginBeanResponse.isError()) {
            Single.error(new Throwable(loginBeanResponse.getError().getErrorMessage()))
        } else { 
            Single.just(loginBeanReponse.getLoginBean()) 
        }})
}
11
anstaendig

Voici une autre tentative. Idée générale: créez un Converter.Factory personnalisé basé sur GsonConverterFactory et un convertisseur Converter<ResponseBody, T> personnalisé basé sur GsonRequestBodyConverter pour analyser le corps entier 2 fois: la première fois sous forme d'erreur et la deuxième fois en tant que type de réponse attendue. De cette manière, nous pouvons analyser les erreurs en un seul endroit tout en préservant une API externe conviviale. Ceci est en fait similaire à la réponse @anstaendig mais avec beaucoup moins de passe-partout: pas besoin de classe supplémentaire de bean wrapper pour chaque réponse et autres choses similaires.

Première classe ServerError qui est un modèle pour votre "erreur JSON" et votre exception personnalisée ServerErrorException afin que vous puissiez obtenir tous les détails.

public class ServerError
{ 
    // add here actual format of your error JSON
    public String errorMsg;
}

public class ServerErrorException extends RuntimeException
{
    private final ServerError serverError;

    public ServerErrorException(ServerError serverError)
    {
        super(serverError.errorMsg);
        this.serverError = serverError;
    }

    public ServerError getServerError()
    {
        return serverError;
    }
}

Évidemment, vous devriez changer la classe ServerError pour qu'elle corresponde à votre format de données actuel.

Et voici la classe principale GsonBodyWithErrorConverterFactory:

public class GsonBodyWithErrorConverterFactory extends Converter.Factory
{
    private final Gson gson;
    private final GsonConverterFactory delegate;
    private final TypeAdapter<ServerError> errorTypeAdapter;


    public GsonBodyWithErrorConverterFactory()
    {
        this.gson = new Gson();
        this.delegate = GsonConverterFactory.create(gson);
        this.errorTypeAdapter = gson.getAdapter(TypeToken.get(ServerError.class));
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    {
        return new GsonBodyWithErrorConverter<>(gson.getAdapter(TypeToken.get(type)));
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit)
    {
        return delegate.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit);
    }

    @Override
    public Converter<?, String> stringConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    {
        return delegate.stringConverter(type, annotations, retrofit);
    }


    class GsonBodyWithErrorConverter<T> implements Converter<ResponseBody, T>
    {
        private final TypeAdapter<T> adapter;

        GsonBodyWithErrorConverter(TypeAdapter<T> adapter)
        {
            this.adapter = adapter;
        }

        @Override
        public T convert(ResponseBody value) throws IOException
        {
            // buffer whole response so we can safely read it twice
            String contents = value.string();

            try
            {
                // first parse response as an error
                ServerError serverError = null;
                try
                {
                    JsonReader jsonErrorReader = gson.newJsonReader(new StringReader(contents));
                    serverError = errorTypeAdapter.read(jsonErrorReader);
                }
                catch (Exception e)
                {
                    // ignore and try to read as actually required type
                }
                // checked that error object was parsed and contains some data
                if ((serverError != null) && (serverError.errorMsg != null))
                    throw new ServerErrorException(serverError);

                JsonReader jsonReader = gson.newJsonReader(new StringReader(contents));
                return adapter.read(jsonReader);
            }
            finally
            {
                value.close();
            }
        }
    }
}

L’idée de base est que l’usine délègue d’autres appels à la variable standard GsonConverterFactory, mais intercepte responseBodyConverter pour créer une GsonBodyWithErrorConverter personnalisée. La GsonBodyWithErrorConverter fait le tour principal:

  1. D'abord, il lit la réponse entière sous forme de chaîne. Ceci est nécessaire pour s'assurer que le corps de la réponse est mis en mémoire tampon afin que nous puissions le relire en toute sécurité 2 fois. Si votre réponse peut contenir des éléments binaires, vous devez les lire en les mettant sous forme binaire. Malheureusement, retrofit2.Utils.buffer n'est pas une méthode publique, mais vous pouvez en créer une vous-même. Je viens de lire le corps comme une chaîne, comme cela devrait fonctionner dans des cas simples.
  2. Créez une jsonErrorReader à partir du corps mis en mémoire tampon et essayez de lire le corps en tant que ServerError. Si nous pouvons le faire, nous avons une erreur, jetez donc notre variable ServerErrorException. Si nous ne pouvons pas le lire dans ce format, ignorez l'exception car il s'agit probablement d'une réponse normale réussie.
  3. Essayez en réalité de lire le corps mis en mémoire tampon (deuxième fois) en tant que type demandé et de le renvoyer.

Notez que si votre format d'erreur réel n'est pas JSON, vous pouvez toujours effectuer les mêmes opérations. Il vous suffit de modifier la logique d’analyse des erreurs dans GsonBodyWithErrorConverter.convert pour personnaliser les éléments dont vous avez besoin.

Alors maintenant, dans votre code, vous pouvez l'utiliser comme suit

.addConverterFactory(new GsonBodyWithErrorConverterFactory()) // use custom factory
//.addConverterFactory(GsonConverterFactory.create()) //old, remove

Note: Je n'ai pas encore essayé ce code, donc il y a peut-être des bugs, mais j'espère que vous avez compris.

1
SergGr

Une solution possible consiste à faire échouer Gson sur des propriétés inconnues. Un problème semble déjà avoir été soulevé ( https://github.com/google/gson/issues/188 ). Vous pouvez utiliser la solution de contournement fournie dans la page de problème. Les étapes sont donc les suivantes:

Ajoutez la solution de contournement ValidatorAdapterFactory à la base de code:

public class ValidatorAdapterFactory implements TypeAdapterFactory {

@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    // If the type adapter is a reflective type adapter, we want to modify the implementation using reflection. The
    // trick is to replace the Map object used to lookup the property name. Instead of returning null if the
    // property is not found, we throw a Json exception to terminate the deserialization.
    TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

    // Check if the type adapter is a reflective, cause this solution only work for reflection.
    if (delegate instanceof ReflectiveTypeAdapterFactory.Adapter) {

        try {
            // Get reference to the existing boundFields.
            Field f = delegate.getClass().getDeclaredField("boundFields");
            f.setAccessible(true);
            Map boundFields = (Map) f.get(delegate);

            // Then replace it with our implementation throwing exception if the value is null.
            boundFields = new LinkedHashMap(boundFields) {

                @Override
                public Object get(Object key) {

                    Object value = super.get(key);
                    if (value == null) {
                        throw new JsonParseException("invalid property name: " + key);
                    }
                    return value;

                }

            };
            // Finally, Push our custom map back using reflection.
            f.set(delegate, boundFields);

        } catch (Exception e) {
            // Should never happen if the implementation doesn't change.
            throw new IllegalStateException(e);
        }

    }
    return delegate;
    }

}

Construisez un objet Gson avec ce TypeAdaptorFactory:

Gson gson = new GsonBuilder().registerTypeAdapterFactory(new ValidatorAdapterFactory()).create()

Et utilisez ensuite cette instance gson dans GsonConverterFactory comme ci-dessous:

apiClient = new Retrofit.Builder()
                .baseUrl(url)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson)) //Made change here
                .client(httpClient)
                .build();

Cela devrait générer une erreur si l'étape unmarshalling trouve une propriété inconnue, dans ce cas la structure de réponse d'erreur.

1
James

Il semble que l'utilisation par Retrofit's de Gson par défaut facilite l'ajout d'une désérialisation personnalisée pour gérer la partie du document JSON à l'origine du problème.

Exemple de code

@FormUrlEncoded
    @POST(GlobalVariables.LOGIN_URL)
    void Login(@Field("email") String key, @Field("password") String value, Callback<Response> callback);

getService().Login(email, password, new MyCallback<Response>(context, true, null)
{
    @Override
    public void failure(RetrofitError arg0)
     {
        // TODO Auto-generated method stub
        UtilitySingleton.dismissDialog((BaseActivity<?>) context);
        System.out.println(arg0.getResponse());
      }

    @Override
    public void success(Response arg0, Response arg1)
    {
         String result = null;
         StringBuilder sb = null;
         InputStream is = null;
         try
         {
                is = arg1.getBody().in();
                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
                sb = new StringBuilder();
                String line = null;
                while ((line = reader.readLine()) != null)
                {
                    sb.append(line + "\n");
                    result = sb.toString();
                    System.out.println("Result :: " + result);
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    });
1
Maheshwar Ligade

Vous pouvez simplement faire cela en faisant cela 

try
{
String error = response.errorBody().string();
error = error.replace("\"", "");
Toast.makeText(getContext(), error, Toast.LENGTH_LONG).show();
}
catch (IOException e)
{
e.printStackTrace();
}
1
M.Saad Lakhan