I'm using Retrofit (in combination with OkHttp and GSON) to communicate with an online webservice. The webservice has a default wrapper around all it's responses, similar to:
{
"resultCode":"OK",
"resultObj":"Can be a string or JSON object / array",
"error":"",
"message":""
}
In this example resultCode
will either be OK
or NO
. Furthermore error
and message
only have any contents when an error has occured while processing the request. And last, but not least, resultObj
will contain the actual result from the call (which is a string in the example, but some calls return a JSON array or a JSON object).
To process this meta data, I created a generic class, like this one:
public class ApiResult<T> {
private String error;
private String message;
private String resultCode;
private T resultObj;
// + some getters, setters etcetera
}
I've also created classes that represent the responses sometimes given in resultObj
and I've defined an interface for use with Retrofit, that looks a bit like this:
public interface SomeWebService {
@GET("/method/string")
ApiResult<String> someCallThatReturnsAString();
@GET("/method/pojo")
ApiResult<SomeMappedResult> someCallThatReturnsAnObject();
}
As long as the request is valid this all works fine. But when an error occurs on the server side, it will still return a resultObj
with a String-type. This causes someCallThatReturnsAnObject
to crash inside the Retrofit RestAdapter / GSON library, with a message like this:
retrofit.RetrofitError: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException:
Expected BEGIN_OBJECT but was STRING at line 1 column 110 path $.resultObj
Now, finally, my questions are:
As you can see, Gson will ignore the unknown fields and simply match the fields that it's able to.
By default, the Gson object does not serialize the fields with null values to JSON. If a field in a Java object is null, Gson excludes it. We can force Gson to serialize null values via the GsonBuilder class.
Define your model like this:
public class ApiResult {
private String error;
private String message;
private String resultCode;
private MyResultObject resultObj;
}
Then, create a TypeAdapterFactory for MyResultObject:
public class MyResultObjectAdapterFactory implements TypeAdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType()!= MyResultObject.class) return null;
TypeAdapter<MyResultObject> defaultAdapter = (TypeAdapter<MyResultObject>) gson.getDelegateAdapter(this, type);
return (TypeAdapter<T>) new MyResultObjectAdapter(defaultAdapter);
}
public class MyResultObjectAdapter extends TypeAdapter<MyResultObject> {
protected TypeAdapter<MyResultObject> defaultAdapter;
public MyResultObjectAdapter(TypeAdapter<MyResultObject> defaultAdapter) {
this.defaultAdapter = defaultAdapter;
}
@Override
public void write(JsonWriter out, MyResultObject value) throws IOException {
defaultAdapter.write(out, value);
}
@Override
public MyResultObject read(JsonReader in) throws IOException {
/*
This is the critical part. So if the value is a string,
Skip it (no exception) and return null.
*/
if (in.peek() == JsonToken.STRING) {
in.skipValue();
return null;
}
return defaultAdapter.read(in);
}
}
}
Finally, register MyResultObjectAdapterFactory for Gson:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new MyResultObjectAdapterFactory())
.create();
Now, when deserializing an ApiResult json with that Gson object, resultObj will be set null if it is a string.
I Hope this solves your problem =)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With