19
votes

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:

  1. Is there an (easy) way to tell GSON that it should just ignore (aka "nullify") a property if it does not match the expected type?
  2. Can I tell GSON to treat empty strings as null?
5

5 Answers

23
votes

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 =)

9
votes

I've had a similar problem and came up with the following solution in the end:

In stead of trying to parse your element into a String or Array, try storing the data to a simple java.lang.Object

This prevents the parsing from crashing or throwing an exception.

eg. with GSON annotations the property of your model would look like this:

@SerializedName("resultObj")
@Expose
private java.lang.Object resultObj;

Next, when accessing your data at runtime, you can check if your resultObj property is an instance of String or not.

if(apiResultObject instanceof String ){
    //Cast to string and do stuff

} else{
    //Cast to array and do stuff

}

Original post: https://stackoverflow.com/a/34178082/3708094

3
votes

First, this is a bad API design that you're dealing with. :-(

You can use a custom JsonDeserializer to handle this case.

Register it with Retrofit:

MyJsonDeserializer deserializer = new MyJsonDeserializer()).create();
final Gson gson = new GsonBuilder().registerTypeAdapter(ApiResult.class, deserializer);
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint(API_URL)
    .setConverter(new GsonConverter(gson))
    .build();
1
votes

I'm reusing my reponse pojo.

In one response it's String and another response it's List<MajicalModel> magicalField so one parsing failed.

I change it to com.google.gson.JsonElement magicalField; it's work for me. This way it parse raw json and also ignore type mismatch.

1
votes

You can use this code as well:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
yourObject = objectMapper.readValue(jsonString, <ClassName>.class);