How to use gson TypeAdapter in Retrofit 2?

混江龙づ霸主 提交于 2019-12-23 03:34:16

问题


I have a working code wherein my retrofit client can retrieve a List of objects(country) from an api. The problem is, the parameter used by the api returns an ARRAY if I used to retrieve all the countries then it returns a single OBJECT when I want to query for a single country. As a result the following exception is shown.

java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 4 column 17 path $.RestResponse.result

By the way, I'm using Retrofit 2.

I tried the accepted answer in this thread How to handle parameters that can be an ARRAY or OBJECT in Retrofit on Android?

But still it won't work. Here is my implementation.

API

http://services.groupkt.com/country/get/all
http://services.groupkt.com/country/get/iso2code/{alpha2_code}

ApiInterface

public interface ApiInterface {

    @GET("country/get/all")
    Call<Example> getCountry();

    @GET("country/get/iso2code/{alpha2_code}")
    Call<Example> searchCountryByIso2Code(@Path("alpha2_code") String alpha2Code);

    @GET("country/get/iso3code/{alpha3_code}")
    Call<Example> searchCountryByIso3Code(@Path("alpha3_code") String alpha3Code);

    @GET("country/search?text={text to search}")
    Call<Example> searchCountry(@Path("text to search") String searchText);
}

CountryTypeAdapter

public class CountryTypeAdapter extends TypeAdapter<RestResponse> {

    private Gson mGson = new Gson();

    @Override
    public void write(JsonWriter jsonWriter, RestResponse restResponse) throws IOException {
        mGson.toJson(restResponse, RestResponse.class, jsonWriter);
    }

    @Override
    public RestResponse read(JsonReader jsonReader) throws IOException {
        RestResponse result;

        jsonReader.beginObject();
        jsonReader.nextName();

        if (jsonReader.peek() == JsonToken.BEGIN_ARRAY) {
            result = new RestResponse((Result[]) mGson.fromJson(jsonReader, Result.class));
        } else if (jsonReader.peek() == JsonToken.BEGIN_OBJECT) {
            result = new RestResponse((Result) mGson.fromJson(jsonReader, Result.class));
        } else {
            throw new JsonParseException("Unexpected token " + jsonReader.peek());
        }

        jsonReader.endObject();
        return result;
    }
}

ApiClient

public class ApiClient {

    public static final String BASE_URL = "http://services.groupkt.com/";
    private static Retrofit mRetrofit;

    public static Retrofit getmRetrofitClient(){

        if (mRetrofit == null) {
            mRetrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create(new GsonBuilder().registerTypeAdapter(CountryTypeAdapter.class, new CountryTypeAdapter()).create()))
                    .build();
        }
        return mRetrofit;
    }

}

EDIT: RestResponse

public class RestResponse {

    @SerializedName("messages")
    @Expose
    private List<String> messages = null;
    @SerializedName("result")
    @Expose
    private List<Result> result = null;

    public RestResponse() {
    }

    public RestResponse(List<String> messages, List<Result> result) {
        this.messages = messages;
        this.result = result;
    }

    public RestResponse(Result... result) {
        this.result = Arrays.asList(result);
    }

    public List<String> getMessages() {
        return messages;
    }

    public void setMessages(List<String> messages) {
        this.messages = messages;
    }

    public List<Result> getResult() {
        return result;
    }

    public void setResult(List<Result> result) {
        this.result = result;
    }


}

回答1:


There are some issues in your code, and some of them can be either fixed or redesigned in order to simplify a lot of things. First, let's take a look at the service (the names differ). Retrofit interface must use @Query for requests with query parameters, not @Path. Also, your service methods must return a Call where its T declare the actual result type giving some information on what's returned by that method. The service you mentioned can return a single element or none (but never HTTP 404 for some reason), or a list depending on endpoint URLs. Let's assume, single values and none values are not meant to be 1- or 0-sized lists:

interface IService {

    @GET("country/get/all")
    Call<List<Country>> getCountries();

    @GET("country/get/iso2code/{code}")
    Call<Country> getCountryByIso2Code(
            @Path("code") String code
    );

    @GET("country/get/iso3code/{code}")
    Call<Country> getCountryByIso3Code(
            @Path("code") String code
    );

    @GET("country/search")
    Call<List<Country>> searchCountries(
            @Query("text") String query
    );

}

Next, the Country class may look as follows:

final class Country {

    @SerializedName("name")
    final String name = null;

    @SerializedName("alpha2_code")
    final String code2 = null;

    @SerializedName("alpha3_code")
    final String code3 = null;

    @Override
    public String toString() {
        return name + '(' + code2 + ',' + code3 + ')';
    }

}

Note that @SerializedName can map an external name to a local field name, and the fields are final and nullified - Gson can deal with them itself. Next, I'm assuming that you don't really care the response structure, and only need $.Response.result that should be adapted respectively. TypeAdapterFactory is what you need and can work with any response, not necessarily for countries:

final class ResponseExtractorTypeAdapterFactory
        implements TypeAdapterFactory {

    private final Gson gson;

    private ResponseExtractorTypeAdapterFactory(final Gson gson) {
        this.gson = gson;
    }

    static TypeAdapterFactory getResponseExtractorTypeAdapterFactory(final Gson gson) {
        return new ResponseExtractorTypeAdapterFactory(gson);
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson responseGson, final TypeToken<T> typeToken) {
        // Using responseGson would result in infinite recursion since this type adapter factory overrides any type
        return new ResponseExtractorTypeAdapter<>(gson, typeToken.getType());
    }

    private static final class ResponseExtractorTypeAdapter<T>
            extends TypeAdapter<T> {

        private final Gson gson;
        private final Type type;

        private ResponseExtractorTypeAdapter(final Gson gson, final Type type) {
            this.gson = gson;
            this.type = type;
        }

        @Override
        public void write(final JsonWriter out, final T value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public T read(final JsonReader in)
                throws IOException {
            T result = null;
            // Strip the top most enclosing { for $
            in.beginObject();
            final String name1 = in.nextName();
            switch ( name1 ) {
            case "RestResponse":
                // RestResponse { for $.Response
                in.beginObject();
                while ( in.hasNext() ) {
                    final String name2 = in.nextName();
                    switch ( name2 ) {
                    case "messages":
                        // If it's just $.Response.message, then skip it
                        in.skipValue();
                        break;
                    case "result":
                        // If it's $.Response.result, then delegate it to "real" Gson
                        result = gson.fromJson(in, type);
                        break;
                    default:
                        throw new MalformedJsonException("Unexpected at $.RestResponse: " + name2);
                    }
                }
                // RestResponse } for $.Response
                in.endObject();
                break;
            default:
                throw new MalformedJsonException("Unexpected at $: " + name1);
            }
            // Strip the top most enclosing } for $
            in.endObject();
            return result;
        }

    }

}

Putting it all together:

public static void main(final String... args) {
    final Gson gson = new GsonBuilder()
            // configure whatever you like
            .create();
    final Gson responseGson = new GsonBuilder()
            .registerTypeAdapterFactory(getResponseExtractorTypeAdapterFactory(gson))
            .create();
    final Retrofit retrofit = new Builder()
            .baseUrl("http://services.groupkt.com/")
            .addConverterFactory(GsonConverterFactory.create(responseGson))
            .build();
    final IService apiInterface = retrofit.create(IService.class);
    apiInterface.getCountries().enqueue(callback("getCountries()")); // http://services.groupkt.com/country/get/all
    apiInterface.getCountryByIso2Code("UA").enqueue(callback("getCountryByIso2Code()")); // http://services.groupkt.com/country/get/iso2code/UA
    apiInterface.getCountryByIso3Code("UKR").enqueue(callback("getCountryByIso3Code()")); // http://services.groupkt.com/country/get/iso3code/UKR
    apiInterface.searchCountries("land").enqueue(callback("searchCountries()")); // http://services.groupkt.com/country/search?text=land
}

private static <T> Callback<T> callback(final String name) {
    return new Callback<T>() {
        @Override
        public void onResponse(final Call<T> call, final Response<T> response) {
            // Just make sure the output is not written in middle
            synchronized ( System.out ) {
                System.out.print(name);
                System.out.print(": ");
                System.out.println(response.body());
                System.out.println();
            }
        }

        @Override
        public void onFailure(final Call<T> call, final Throwable ex) {
            ex.printStackTrace(System.err);
        }
    };
}

And the result (assuming the responses are received and processed sequentally + the long responses are cut):

getCountries(): [Afghanistan(AF,AFG), Åland Islands(AX,ALA), Albania(AL,ALB), ..., Yemen(YE,YEM), Zambia(ZM,ZMB), Zimbabwe(ZW,ZWE)]

getCountryByIso2Code(): Ukraine(UA,UKR)

getCountryByIso3Code(): Ukraine(UA,UKR)

searchCountries(): [Åland Islands(AX,ALA), Bouvet Island(BV,BVT), Cayman Islands(KY,CYM), ..., United States Minor Outlying Islands(UM,UMI), Virgin Islands (British)(VG,VGB), Virgin Islands (U.S.)(VI,VIR)]



来源:https://stackoverflow.com/questions/42642933/how-to-use-gson-typeadapter-in-retrofit-2

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!