Join FlipAndroid.COM Telegram Group: https://t.me/joinchat/F_aqThGkhwcLzmI49vKAiw


Retrofit2 Condición de manejar cuando el código de estado 200 pero la estructura de json diferente a la clase de datamodel

Estoy utilizando Retrofit2 y RxJava2CallAdapterFactory .

La API que consume devuelve el código de estado siempre como 200 y para el éxito y la respuesta json string la estructura json es completamente diferente. Como el código de estado siempre es 200, el método onResponse () se llama siempre. Por lo tanto, no puedo extraer msgs del error del json en la condición del error.

  • Definiendo el orden Z de vistas de RelativeLayout en Android
  • Cómo agregar consistentemente fondos alternativos a las vistas de elementos en RecyclerView
  • Syncadapter onPerformSync que se llama dos veces la primera vez
  • Lollipop Barra de progreso Tinción
  • Disposición de Android alineación izquierda y derecha en disposición horizontal
  • Android - NotificationCompat.Builder que apila las notificaciones con setGroup (grupo) que no funciona
  • Solución 1:

    Utilizo ScalarsConverterFactory para obtener la cadena de respuesta y uso Gson manualmente para analizar la respuesta. Cómo obtener respuesta como String utilizando retrofit sin utilizar GSON o cualquier otra biblioteca en android

    Problema con esta solución : Estoy planeando usar RxJava2CallAdapterFactory para que el método retrofit devuelva clase DataModel.

    Necesito encontrar la mejor solución para este problema, de manera que puedo seguir devolviendo las clases de modelo de datos desde el método Retrofit y de alguna manera identificar la condición de error de la respuesta (identificar la respuesta json no coincide con el modelo de datos) y luego analizar el error Json en un modelo de datos.

    Cliente de adaptación

      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étodo

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

    Interfaz

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

    PS: La API no puede cambiar por ahora, ya que algunas otras aplicaciones la están consumiendo.

    • El analizador de Gson no devuelve una instancia de objeto nulo para que entienda que hay una estructura de json y una incompatibilidad de datamodel.

    • RestAdapter está obsoleto en Retrofit 2

    Estoy buscando el mejor enfoque para resolver esto, preferiblemente evitar manualmente json parsing y tomar la mayoría de la ventaja de retrofit y RX adaptadores.

    EDITAR

    Código de respuesta 200

    1. response.isSuccessful() == true

    2. response.body() != null también es cierto, ya que Gson nunca crea una instancia null o lanza una excepción si hay desajuste de la estructura de json

    3. response.errorBody() == null en todo momento como respuesta enviada como flujo de entrada desde el servidor.

       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

    SOLUCIÓN

    La solución se basa en la respuesta anstaendig

    • He creado una clase genérica básica para promover esta respuesta.
    • Ya que tengo múltiples apis y modelos de datos que tengo que crear deserilizers para cada uno

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

    DESERIALIZADOR BASE

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

    Deserializador personalizado para cada 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); } } 

    GRANJA DE RESPUESTA PERSONALIZADA

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

    CLIENTE

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

    RESPUESTA DEL MANIPULADOR

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

  • Llamar setVolumeControlStream desde un servicio
  • Icono de notificación en blanco para Android con Phonegap Build y PushPlugin
  • Imágenes de fondo libres de derechos para la aplicación Android
  • Android: Geocodificación inversa - getFromLocation
  • Android: dither = "true" no dither, lo que está mal?
  • Inheriting AppCompat 22.1.1 El cuadro de diálogo colorAccent del tema de la aplicación no funciona
  • 5 Solutions collect form web for “Retrofit2 Condición de manejar cuando el código de estado 200 pero la estructura de json diferente a la clase de datamodel”

    Por lo tanto, tiene dos respuestas satisfactorias (código de estado 200) del mismo punto final. Uno que es el modelo de datos real y uno que es un error (ambos como una estructura del json como esto ?:

    Valid LoginBean respuesta:

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

    Respuesta de error

     { "error": "error message" } 

    Lo que puede hacer es tener una entidad que envuelva ambos casos y utilice un deserializador personalizado.

     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 contenedor para el error:

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

    Entonces necesitas un 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); } } } 

    A continuación, agregue este deserializador al 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(); 

    Esta es la única manera en que puedo pensar en hacer este trabajo. Pero como ya se mencionó este tipo de diseño de la API es simplemente mal porque los códigos de estado están ahí por una razón. Todavía espero que esto ayude.

    EDIT: lo que puede hacer dentro de la clase en la que realiza la llamada a Retrofit (si ya ha convertido de Call<LoginBeanResponse> a Single<LoginBeanResponse> con RxJava) es realmente devolver un error correcto. Algo como:

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

    Parece que Retrofit's uso de Retrofit's de Gson por defecto hace que sea fácil agregar un deserializar personalizado para manejar la parte del documento JSON que fue el problema.

    Código de muestra

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

    Simplemente puede hacerlo haciendo esto

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

    Una posible solución es hacer que Gson falla en propiedades desconocidas. Parece que ya se ha planteado un problema ( https://github.com/google/gson/issues/188 ). Puede utilizar la solución proporcionada en la página de problemas. Así que los pasos son los siguientes:

    Agregue la solución ValidatorAdapterFactory a la base de código:

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

    Construir un objeto Gson con este TypeAdaptorFactory:

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

    Y luego use esta instancia de Gson en GsonConverterFactory como a continuación:

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

    Esto debería arrojar un error si el paso unmarshalling encuentra una propiedad desconocida, en este caso la estructura de respuesta de error.

    Aquí hay otro intento. Idea general: crear un Converter.Factory personalizado basado en GsonConverterFactory y un Converter<ResponseBody, T> personalizado Converter<ResponseBody, T> convertidor basado en GsonRequestBodyConverter para analizar cuerpo entero 2 veces: primera vez como error y segunda vez como tipo de respuesta esperada real. De esta manera podemos analizar el error en un solo lugar y aún conservar una API externa amigable. Esto es realmente similar a @anstaendig respuesta pero con mucho menos boilerplate: no hay necesidad de la clase de envoltura de habas adicionales para cada respuesta y otras cosas similares.

    Primera clase ServerError que es un modelo para su "error JSON" y una excepción personalizada ServerErrorException para que pueda obtener todos los detalles

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

    Obviamente debe cambiar la clase ServerError para que coincida con su formato de datos real.

    Y aquí está la clase principal 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(); } } } } 

    La idea básica es que la fábrica delega otras llamadas a la GsonConverterFactory estándar, pero intercepta responseBodyConverter para crear un GsonBodyWithErrorConverter personalizado. El GsonBodyWithErrorConverter está haciendo el truco principal:

    1. Primero lee la respuesta completa como String. Esto es necesario para asegurar que el cuerpo de la respuesta esté almacenado en búfer para poder volver a leerlo 2 veces. Si su respuesta realmente puede contener algún binario que debe leer y almacenar en búfer la respuesta como binario y desafortunadamente retrofit2.Utils.buffer no es un método público, pero puede crear uno similar. Acabo de leer el cuerpo como una cadena, ya que debería funcionar en casos simples.
    2. Cree un jsonErrorReader del cuerpo almacenado en búfer e intente leer el cuerpo como un ServerError . Si podemos hacerlo, tenemos un error para lanzar nuestro ServerErrorException personalizado. Si no podemos leerlo en ese formato – solo ignora la excepción ya que es probablemente la respuesta correcta
    3. En realidad intenta leer el cuerpo almacenado en búfer (segunda vez) como el tipo solicitado y devolverlo.

    Tenga en cuenta que si su formato de error real no es JSON todavía puede hacer todo lo mismo. Sólo tiene que cambiar la lógica de análisis de errores dentro de GsonBodyWithErrorConverter.convert a cualquier cosa que necesite.

    Tan ahora en su código usted puede utilizarlo como siguiente

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

    Nota: En realidad no he probado este código por lo que puede haber errores, pero espero que se la idea.

    FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.