问题
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 null
ified - 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