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
Here is another attempt. General idea: create a custom Converter.Factory based on GsonConverterFactory and a custom Converter 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 errorTypeAdapter;
public GsonBodyWithErrorConverterFactory()
{
this.gson = new Gson();
this.delegate = GsonConverterFactory.create(gson);
this.errorTypeAdapter = gson.getAdapter(TypeToken.get(ServerError.class));
}
@Override
public Converter 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 implements Converter
{
private final TypeAdapter adapter;
GsonBodyWithErrorConverter(TypeAdapter 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:
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.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 responseNote 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.