Android - Retrofit 2 Custom Error Response Handling

Usually, when using Retrofit 2, we have two callback listeners: onResponse and onFailure If onResponse is called, it doesn't always mean that we get the success condition. Usually a response is considered success if the status scode is 2xx and Retrofit has already provides isSuccessful() method. The problem is how to parse the response body which most likely has different format. In this tutorial, I'm going to show you how to parse custom JSON response body in Retrofit 2.

Preparation

First, we create RetrofitClientInstance class for creating new instance of Retrofit client and the MyService interface which defines the endpoint we're going to call.

RetrofitClientInstance.java

  public class RetrofitClientInstance {
      private static final String BASE_URL = "http://159.89.185.115:3500";

      public static Retrofit getRetrofitInstance() {
            return new retrofit2.Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
      }
  }

MyService.java

  public interface MyServie {
      @GET("/api/items")
      Call<List> getItems();
  }

Let's say we have an endpoint /api/items which in normal case, it returns the list of Item objects.

Item.java

  public class Item {
      private String id;
      private String name;

      public Item(String id, String name) {
          this.id = id;
          this.name = name;
      }

     /* Getter and Setter here */
  }

Below is the standard way to call the endpoint.

Example.java

  MyService myService = RetrofitClientInstance
          .getRetrofitInstance()
          .create(MyService.class);
  Call<List<Item>> call = retailService.getItems();

  call.enqueue(new Callback<List<Item>>() {
      @Override
      public void onResponse(final Call<List<Item>> call, final Response<List<Item>> response) {
          List<Item> items = response.body();
          Toast.makeText(getContext(), "Success").show();
      }

      @Override
      public void onFailure(final Call call, final Throwable t) {
          Toast.makeText(getContext(), "Failed").show();
      }
  });

Parsing Error Response

The problem is how to parse the response if it's not success. The idea is very simple. We need to create custom util for parsing the error. For example, we know that the server will return a JSON object with the following structure when an error occurs.

  {
    "success": false,
    "errors": [
      "Error message 1",
      "Error message 2"
    ]
  }

We need to create a util for parsing the error. It returns an Object containing parsed error body. First, we define the class which represents the parsed error.

APIError.java

  public class APIError {
      private boolean success;
      private ArrayList messages;

      public static class Builder {
          public Builder() {}

          public Builder success(final boolean success) {
              this.success = success;
              return this;
          }

          public Builder messages(final ArrayList messages) {
              this.messages = messages;
              return this;
          }

          public Builder defaultError() {
              this.messages.add("Something error");
              return this;
          }

          public APIError build() { return new APIError(this); }
      }

      private APIError(final Builder builder) {
          success = builder.successs;
          messages = builder.messages;
      }
  }

And here's the util for parsing the response body. If you have different response body format, you can adjust it to suit your case.

ErrorUtils.java

  public class ErrorUtils {
      public static APIError parseError(final Response<?> response) {
          JSONObject bodyObj = null;
          boolean success;
          ArrayList messages = new ArrayList<>();

          try {
              String errorBody = response.errorBody().string();

              if (errorBody != null) {
                  bodyObj = new JSONObject(errorBody);

                  success = bodyObj.getBoolean("success");
                  JSONArray errors = bodyObj.getJSONArray("errors");

                  for (int i = 0; i < errors.length(); i++) {
                      messages.add(errors.get(i));
                  }
              } else {
                  success = false;
                  messages.add("Unable to parse error");
              }
          } catch (Exception e) {
              e.printStackTrace();

              success = false;
              messages.add("Unable to parse error");
          }

          return new APIError.Builder()
                  .success(false)
                  .messages(messages)
                  .build();
      }
  }

Finally, change the onResponse and onFailure methods. If response.isSuccessful() is false, we use the error parser. In addition, if the code go through onFailure which most likely we're even unable to get the error response body, just return the default error.

Example.java

  MyService myService = RetrofitClientInstance
          .getRetrofitInstance()
          .create(MyService.class);
  Call<List<Item>> call = retailService.getItems();

  call.enqueue(new Callback<List<Item>>() {
      @Override
      public void onResponse(final Call<List<Item>> call, final Response<List<Item>> response) {
          if (response.isSuccessful()) {
              List<Item> items = response.body();
              Toast.makeText(getContext(), "Success").show();
          } else {
              apiError = ErrorUtils.parseError(response);
              Toast.makeText(getContext(), R.string.cashier_create_failed,
                      Toast.LENGTH_LONG).show();
          }
      }

      @Override
      public void onFailure(final Call<Cashier> call, final Throwable t) {
          apiError = new APIError.Builder().defaultError().build();
          Toast.makeText(getContext(), "failed").show();
      }
  });

That's how to parse error body in Retrofit 2. If you also need to define custom GSON converter factory, read this tutorial.