Retrofit2 условие дескриптора, когда код состояния 200, но структура json отличается от класса datamodel

я использую Retrofit2 и RxJava2CallAdapterFactory.

API, который я потребляю, возвращает код состояния всегда как 200, а для строки JSON успеха и ответа структура json совершенно другая. Поскольку код состояния всегда равен 200, метод onResponse () вызывается всегда. Следовательно, я не могу извлечь ошибки msgs из json в состоянии ошибки.

Решение 1:

я использую ScalarsConverterFactory получить строку ответа и вручную использовать Gson для разбора ответ. как получить ответ в виде строки с помощью дооснащения без использования GSON или любой другой библиотеки в android

проблема с этим решением: я планирую использовать RxJava2CallAdapterFactory для того, чтобы метод retrofit возвращал класс DataModel.

мне нужно найти лучшее решение для этой проблемы, таким образом, я могу продолжать возвращать классы модели данных из метода дооснащения и каким-то образом я определяю условие ошибки из ответа (определите ответ json не соответствует модели данных), а затем проанализируйте ошибку json в модели данных.

Клиент Retrofit

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

метод

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

интерфейс

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

PS : API не может измениться на данный момент, так как некоторые другие приложения потребляют его.

  • Gson parser не возвращает экземпляр нулевого объекта для меня, чтобы понять, что существует несоответствие структуры json и datamodel.

  • RestAdapter устарел в дооснащении 2

Я ищу лучший подход для решения этой проблемы, желательно избегать разбора вручную json и использовать большинство преимуществ адаптеров retrofit и RX.

редактировать

код ответа 200, следовательно,

  1. response.isSuccessful() == true

  2. response.body() != null тоже true as Gson никогда не создает экземпляр null или выдает исключение, если есть несоответствие структуры json

  3. response.errorBody() == null во все времена как ответ, отправленный в виде входного потока с сервера.

    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
    }
    

Изменить 2

решение

решение основано на ответе anstaendig

  • я создал базовый универсальный класс для дальнейшего ответа.
  • поскольку у меня есть несколько API и моделей данных, я должен создать десерилизаторы для каждого

БАЗОВЫЙ 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;
    }
}

БАЗОВЫЙ ДЕСЕРИАЛИЗАТОР

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

пользовательский десериализатор для каждого 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

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

}

клиент

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

ОТВЕТ РУЧКИ

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

5 ответов


таким образом, у вас есть два разных успешных ответа (код состояния 200) от одной и той же конечной точки. Один из них является фактической моделью данных, а другой-ошибкой (как структура json, как это?:

действительный ответ LoginBean:

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

сообщение об ошибке

{
  "error": "error message"
}

что вы можете сделать, это иметь сущность, которая обертывает оба случая и использует пользовательский десериализатор.

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
}

обертка для ошибки:

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

тогда вам нужно 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);
    }
  }
}

затем добавьте этот десериализатор в 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();

это единственный способ, который я могу придумать, чтобы это сработало. Но, как уже упоминалось, такой дизайн API просто неправильный, потому что коды состояния существуют по какой-то причине. Я все еще надеюсь, что это помогает.

EDIT: что вы можете сделать внутри класса, где вы делаете вызов этой модернизации (если вы уже преобразованы из Call<LoginBeanResponse> to Single<LoginBeanResponse> С RxJava) фактически возвращает правильное ошибка. Что-то вроде:

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

кажется, что Retrofit's использование Gson по умолчанию позволяет легко добавить пользовательскую десериализацию для обработки части JSON документ, который был проблемой.

пример кода

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

вот еще одна попытка. Общая идея: создайте пользовательский Converter.Factory на основе GsonConverterFactory и обычай Converter<ResponseBody, T> преобразователя на основе GsonRequestBodyConverter для разбора всего тела 2 раза: первый раз как ошибка и второй раз как фактический ожидаемый тип ответа. Таким образом, мы можем анализировать ошибки в одном месте и по-прежнему сохранять дружественный внешний API. Это на самом деле похоже на ответ @anstaendig, но с гораздо меньшим шаблоном: нет необходимости в дополнительном классе bean-оболочки для каждого ответа и других подобных материал.

первый класс ServerError это модель для вашей "ошибки JSON" и пользовательского исключения ServerErrorException таким образом, вы можете получить все детали

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

очевидно, вы должны изменить ServerError класс, соответствующий вашему фактическому формату данных.

а вот и основной класс 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();
            }
        }
    }
}

основная идея заключается в том, что фабрика делегирует другие вызовы стандарту GsonConverterFactory но перехватывает responseBodyConverter создать пользовательское GsonBodyWithErrorConverter. The GsonBodyWithErrorConverter is делаем главный трюк:

  1. сначала он читает весь ответ в строку. Это необходимо для обеспечения буферизации тела ответа, чтобы мы могли безопасно перечитывать его 2 раза. Если ваш ответ на самом деле может содержать двоичный файл, вы должны прочитать и буферизировать ответ как двоичный и, к сожалению,retrofit2.Utils.buffer не публичный метод, но вы можете создать подобное себе. Я просто читаю тело как строку, поскольку оно должно работать в простых случаях.
  2. создать jsonErrorReader от буферизованное тело и попробуйте прочитать тело как ServerError. Если мы можем это сделать, у нас есть ошибка, поэтому бросьте наш custom ServerErrorException. Если мы не можем прочитать его в этом формате-просто игнорируйте исключение, поскольку это, вероятно, просто обычный успешный ответ
  3. фактически попробуйте прочитать буферизованное тело (второй раз) как запрошенный тип и вернуть его.

обратите внимание, что если ваш фактический формат ошибки не JSON, вы все равно можете делать все то же самое. Вам просто нужно изменить синтаксический анализ ошибок логика внутри GsonBodyWithErrorConverter.convert ко всему, что вам нужно.

Итак, в вашем коде вы можете использовать его следующим образом

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

примечание: Я не пробовал этот код, поэтому могут быть ошибки, но надеюсь вы поняли.


вы можете просто сделать это, делать это

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

одним из возможных решений является сбой Gson на неизвестных свойствах. Кажется, уже поднят вопрос (https://github.com/google/gson/issues/188). Можно использовать обходной путь, указанный на странице проблема. Итак, шаги следующие:

добавьте обходной путь ValidatorAdapterFactory в базу кода:

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

}

создайте объект Gson с помощью этого TypeAdaptorFactory:

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

а затем используйте этот экземпляр gson в GsonConverterFactory, как показано ниже:

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

Это должно вызвать ошибку, если шаг unmarshalling находит неизвестное свойство, в этом случае структуру ответа на ошибку.