Retrofit2 Handle condition when status code 200 but json structure different than datamodel class

后端 未结 5 443
-上瘾入骨i
-上瘾入骨i 2021-02-01 22:09

I\'m using Retrofit2 and RxJava2CallAdapterFactory.

The API I consume returns status code always as 200 and for success and response JSON string the JSON st

相关标签:
5条回答
  • 2021-02-01 22:49

    So you have two different successful (status code 200) responses from the same endpoint. One being the actual data model and one being an error (both as a json structure like this?:

    Valid LoginBean response:

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

    Error response

    {
      "error": "error message"
    }
    

    What you can do is have an entity that wraps both cases and use a custom deserializer.

    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
    }
    

    A wrapper for the error:

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

    Then you need a 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);
        }
      }
    }
    

    Then add this deserializer to the 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();
    

    This is the only way I can think of making this work. But as already mentioned this kind of API design is just wrong because status codes are there for a reason. I still hope this helps.

    EDIT: What you can then do inside the class where you make the call to that Retrofit (if you already converted from Call<LoginBeanResponse> to Single<LoginBeanResponse> with RxJava) is actually return a proper error. Something like:

    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()) 
            }})
    }
    
    0 讨论(0)
  • 2021-02-01 22:54

    You can simply do that by doing this

    try
    {
    String error = response.errorBody().string();
    error = error.replace("\"", "");
    Toast.makeText(getContext(), error, Toast.LENGTH_LONG).show();
    }
    catch (IOException e)
    {
    e.printStackTrace();
    }
    
    0 讨论(0)
  • 2021-02-01 22:55

    It seems out that Retrofit's use of Gson by default makes it easy to add a custom deserialize to handle the portion of the JSON document that was the problem.

    Sample 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();
                }
            }
        });
    
    0 讨论(0)
  • 2021-02-01 23:04

    Here is another attempt. General idea: create a custom Converter.Factory based on GsonConverterFactory and a custom Converter<ResponseBody, T> converter based on GsonRequestBodyConverter to parse whole body 2 times: first time as error and second time as actual expected response type. In this way we can parse error in a single place and still preserve friendly external API. This is actually similar to @anstaendig answer but with much less boilerplate: no need for additional wrapper bean class for each response and other similar stuff.

    First class ServerError that is a model for your "error JSON" and custom exception ServerErrorException so you can get all the details

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

    Obviously you should change the ServerError class to match your actual data format.

    And here is the main class 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();
                }
            }
        }
    }
    

    The basic idea is that the factory delegates other calls to the standard GsonConverterFactory but intercepts responseBodyConverter to create a custom GsonBodyWithErrorConverter. The GsonBodyWithErrorConverter is doing the main trick:

    1. First it reads whole response as String. This is required to ensure response body is buffered so we can safely re-read it 2 times. If your response actually might contain some binary you should read and buffer the response as binary and unfortunately retrofit2.Utils.buffer is not a public method but you can create a similar one yourself. I just read the body as a String as it should work in simple cases.
    2. Create a jsonErrorReader from the buffered body and try to read the body as a ServerError. If we can do it, we've got an error so throw our custom ServerErrorException. If we can't read it in that format - just ignore exception as it is probably just normal successful response
    3. Actually try to read the buffered body (second time) as the requested type and return it.

    Note that if your actual error format is not JSON you still can do all the same stuff. You just need to change the error parsing logic inside GsonBodyWithErrorConverter.convert to anything custom you need.

    So now in your code you can use it as following

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

    Note: I haven't actually tried this code so there might be bugs but I hope you get the idea.

    0 讨论(0)
  • 2021-02-01 23:08

    One possible solution is to make Gson fail on unknown properties. There seems to be an issue raised already(https://github.com/google/gson/issues/188). You can use the workaround provided in the issue page. So the steps are as follows:

    Add the workaround ValidatorAdapterFactory to the code base:

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

    Build a Gson object with this TypeAdaptorFactory:

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

    And then use this gson instance in GsonConverterFactory like below:

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

    This should throw an error if the unmarshalling step finds an unknown property, in this case the error response structure.

    0 讨论(0)
提交回复
热议问题