I am using an API that has basically 2 returns:
A single object:
{
"data": {Foo}
}
Or a list of objects:
{
"data": [
{Bar},
{Bar},
...
]
}
So, I have created 2 envelope class
class Envelope<T> {
private T data;
public T getData() {
return data;
}
}
class EnvelopeList<T> {
private List<T> data;
public List<T> getData() {
return data;
}
}
and the service interface
#GET(PATH)
Call<Envelope<Foo>> get();
#GET(PATH_LIST)
Call<EnvelopeList<Bar>> getList();
Using the retrofit 2 config
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create());
everything is working fine...
If I remove the envelope from service like this
#GET(PATH)
Call<Foo> get();
#GET(PATH_LIST)
Call<List<Bar>> getList();
the first that return only an object still working but the one that returns a List gives the error java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
So, I tried to create a converter
public class EnvelopeListConverterFactory extends Converter.Factory {
#Override
public Converter<ResponseBody, ?> responseBodyConverter(
final Type type,
Annotation[] annotations,
Retrofit retrofit) {
Type envelopeType = new ParameterizedType() {
#Override
public Type[] getActualTypeArguments() {
return new Type[]{type};
}
#Override
public Type getRawType() {
return null;
}
#Override
public Type getOwnerType() {
return EnvelopeList.class;
}
};
Converter<ResponseBody, EnvelopeList> delegate =
retrofit.nextResponseBodyConverter(this, envelopeType, annotations);
return new EnvelopeListConverter(delegate);
}
}
public class EnvelopeListConverter<T> implements Converter<ResponseBody, List<T>> {
final Converter<ResponseBody, EnvelopeList<T>> delegate;
EnvelopeListConverter(Converter<ResponseBody, EnvelopeList<T>> delegate) {
this.delegate = delegate;
}
#Override
public List<T> convert(ResponseBody responseBody) throws IOException {
EnvelopeList<T> envelope = delegate.convert(responseBody);
return envelope.getData();
}
}
and if I create the retrofit build like this
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(new EnvelopeListConverterFactory());
I still get the same error as before, but if invert the converter order like
Retrofit.Builder builder = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(new EnvelopeListConverterFactory())
.addConverterFactory(GsonConverterFactory.create());
the error change to java.lang.IllegalArgumentException: Unable to create a converter for class Bar for method Service.getList and the one that returns a single Object, start to give the same error too.
What can I do to make the service return the Objects without the envelope?
Update
I think I passed the wrong idea on my question. The problem is not that the request can return a single object or a list. I just trying to pass thru the envelope and get the data directly.
I trying to do this http://f2prateek.com/2017/04/21/unwrapping-data-with-retrofit-2/ but it is not working
I finally found how to create a correct converter class to not need to use the envelope/wrapper on every request.
https://hackernoon.com/retrofit-converter-for-wrapped-responses-8919298a549c
Related
I use retrofit 2 and I have UserService with rest methods which return objects Call.
I would like to invoke these methods and return just data object.
I have this:
#GET("users")
Call<List<UserDTO>> getUsers();
but I want:
#GET("users")
List<UserDTO> getUsers();
I know that was possible by default in retrofit 1.9 but i couldn't find solution for this problem.
I dont want invoke method, execute call, get body and make try..catch every time when I use it.
When I invoke method from my second example I receive error:
Could not locate call adapter for java.util.List<>
Is it possible to handle this case in any adapter? And how to do it ?
I resolved this problem like that:
public class CustomCallAdapter<T> implements CallAdapter<T, T> {
private Type returnType;
public CustomCallAdapter(Type returnType) {
this.returnType = returnType;
}
#Override
public Type responseType() {
return returnType;
}
#Override
public T adapt(Call<T> call) {
try {
return call.execute().body();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static class Factory extends CallAdapter.Factory {
#Override
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
return new CustomCallAdapter(returnType);
}
}
}
I've been working with Retrofit on a couple of my projects before but now I want to do something slightly different. I'm calling an api that wraps my response in a structure similar to this:
{ // only for demo purposes. Probably errors and data will never be populated together
"body": {
"errors": {
"username": [
"Username is too short",
"Username already exists"
]
},
"data": {
"message": "User created."
}
}
}
I'm trying to convert all that to a generic class which will wrap that response for me. What I have in mind is something like
public class ApiResponse<T> {
private T data;
private Map<String, List<String>> errors;
public ApiResponse(T data, Map<String, List<String>> errors) {
this.data = data;
this.errors = errors;
}
}
Where T can be any class.
I tried implementing a JsonDeserializer<ApiResponse<T>> based on some examples I found around the internet but I can't wrap my head around how to make it work as much automatically as possible and let Retrofit and Gson do the heavy lifting
My Converter class is as follows:
public class ApiResponseDeserializer<T> implements JsonDeserializer<ApiResponse<T>> {
private Class clazz;
public ApiResponseDeserializer(Class clazz) {
this.clazz = clazz;
}
#Override
public ApiResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final JsonObject body = jsonObject.getAsJsonObject("body");
final JsonObject errors = body.getAsJsonObject("errors");
final JsonObject data = body.getAsJsonObject("data");
Map<String, List<String>> parsedErrors = new HashMap<>();
for(String key : errors.keySet()) {
List<String> errorsList = new ArrayList<>();
JsonArray value = errors.getAsJsonArray(key);
Iterator<JsonElement> valuesIterator = value.iterator();
while(valuesIterator.hasNext()) {
String error = valuesIterator.next().getAsString();
errorsList.add(error);
}
parsedErrors.put(key, errorsList);
}
T parsedData = context.deserialize(data, clazz);
return new ApiResponse<T>(parsedData, parsedErrors);
}
}
and then when building my retrofit client
public static Retrofit getClient() {
if (okHttpClient == null) {
initOkHttp();
}
Gson gson = new GsonBuilder()
.registerTypeAdapter(ApiResponse.class, new ApiResponseDeserializer<>(......) // PROBLEM
.create();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(Const.API_BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit;
}
But I feel like it's not generic enough to be able to convert my classes automatically. And also I have no idea how should I hint Gson what type my data.
My endpoints are defined as follows:
#POST("users/signup")
Single<ApiResponse<RegisterResponseData>> register(#Body RegisterRequest request);
But how do I make a generic Retrofit instance with a generic Gson type adapter that knows how to convert my response to a ApiResponse<RegisterResponseData>? And knows that the data property from the response should be converted to an object of type RegisterResponseData...
When you specify return type in Retrofit's client it's passed to Retrofit's converter as Type and then Gson receives that type which will be your ApiResponse<RegisterResponseData>. From that point Gson will understand that data is of type RegisterResponseData and will produce your model object.
Just try it without your ApiResponseDeserializer and you'll see it's working.
Edit:
Answering your additional question in comments:
If you want to skip your "body" object in json you can write your wrapper object like this:
public class ApiResponse<T> {
#SerializedName("body")
private ApiResponseBody<T> body;
public ApiResponse() {
}
public ApiResponse(ApiData<T> body) {
this.body = body;
}
}
public class ApiResponseBody<T> {
#SerializedName("data")
private T data;
#SerializedName("errors")
private Map<String, List<String>> errors;
public ApiResponseBody() {
}
public ApiResponseBody(T data, Map<String, List<String>> errors) {
this.data = data;
this.errors = errors;
}
}
And use it in usual way
#POST("users/signup")
Single<ApiResponse<RegisterResponseData>> register(#Body RegisterRequest request);
I'm using Retrofit to make API call, When I handle the response I get the next error (Need to get the data from the API call) -
Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
I don't know if I'm doing it right. Anyway here's my code.
Here's the url link: https://data.gov.il/api/
Retrofit call -
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Result> getRecords();
Retrofit base call -
private static Retrofit retrofit;
public static final String BASE_URL = "https://data.gov.il/api/action/";
public static Retrofit getRetrofitInstance() {
if (retrofit == null) {
retrofit = new retrofit2.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
Model class -
public class Result {
#SerializedName("include_total")
#Expose
private Boolean includeTotal;
#SerializedName("resource_id")
#Expose
private String resourceId;
#SerializedName("fields")
#Expose
private List<Field> fields = null;
#SerializedName("records_format")
#Expose
private String recordsFormat;
#SerializedName("records")
#Expose
private List<Record> records = null;
#SerializedName("limit")
#Expose
private Integer limit;
#SerializedName("_links")
#Expose
private Links links;
#SerializedName("total")
#Expose
private Integer total;
public Boolean getIncludeTotal() {
return includeTotal;
}
public void setIncludeTotal(Boolean includeTotal) {
this.includeTotal = includeTotal;
}
public String getResourceId() {
return resourceId;
}
public void setResourceId(String resourceId) {
this.resourceId = resourceId;
}
public List<Field> getFields() {
return fields;
}
public void setFields(List<Field> fields) {
this.fields = fields;
}
public String getRecordsFormat() {
return recordsFormat;
}
public void setRecordsFormat(String recordsFormat) {
this.recordsFormat = recordsFormat;
}
public List<Record> getRecords() {
return records;
}
public void setRecords(List<Record> records) {
this.records = records;
}
...
Main Activity -
RecallService service = RetrofitClientInstance.getRetrofitInstance().create(RecallService.class);
Call<Result> records = service.getRecords();
records.enqueue(new Callback<Result>() {
#Override
public void onResponse(Call<Result> call, Response<Result> response) {
Log.d(TAG, String.valueOf(response.body().getRecords().get(0).getId())); // ERROR
}
#Override
public void onFailure(Call<Result> call, Throwable t) {
Log.e(TAG, t.getMessage());
}
});
The response, which you are getting from the API, doesn't fit the Result POJO.
The response you get from API is like below:
{
"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search",
"success": true,
"result": {...}
}
By using the Result POJO, you are assuming that you get the response as below, which is a json inside the actual response json, and is not what you actually receive. So, just create a POJO which fairly represents the actual response.
{
"include_total": true,
"resource_id": "2c33523f-87aa-44ec-a736-edbb0a82975e",
"fields": [...],
"records_format": "objects",
"records":[...]
}
Try making a class like below (set the annotations yourself):
class Resp{
Result result;
}
Replace the class Result with Resp, like below and other usages:
#GET("datastore_search?resource_id=2c33523f-87aa-44ec-a736-edbb0a82975e")
Call<Resp> getRecords();
Then, finally you can do:
response.body().getResult().getRecords()
The API link you've shared returns the response in the format below:
{"help": "https://data.gov.il/api/3/action/help_show?name=datastore_search", "success": true, "result": {...}}
You are setting the response object to be of type Result which is actually a sub-element within the root element help in the json response. response.body() would include help and the result would be it's sub-element. Since it is not parsed correctly, you're getting a null response.
You will need to include the root element in your model class and update the API call to use that class type as the response type.
I am trying to use Retrofit and RxJava to make an API call within a custom view in an app that I am working on, but I encounter an incompatible type error when trying to subscribe to the Observable from my Retrofit API call.
My retrofit interface:
public interface ApiQueryInterface{
// Request method and URL specified in the annotation
// Callback for the parsed response is the last parameter
#GET("users/")
Observable<Users> getUsers (
#Query("key") String key,
#Query("address") String address
);
#GET("posts/")
Observable<Posts> getPosts (
#Query("key") String key,
#Query("address") String address
);
}
and the Retrofit call located within the onFinishInflate() of the custom view:
// Create RxJava adapter for synchronous call
RxJava2CallAdapterFactory rxAdapter = RxJava2CallAdapterFactory.create();
// Create Retrofit2 instance for API call
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(rxAdapter)
.build();
// Make API call using retrofit
final ApiQueryInterface apiQueryInterface = retrofit.create(ApiQueryInterface.class);
// API return type defined by interface
Observable<Users> query = apiQueryInterface
.getUsers(KEY, ADDRESS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<Users>() {
#Override
public void onSubscribe(Disposable d) {
}
#Override
public void onNext(Users users) {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onComplete() {
}
});
}
When I build the project I hit an incompatible types error in the custom view on the line beginning with Observable<Users> query = ...:
Error:(60, 27) error: incompatible types: void cannot be converted to Observable<Users>
"Users" is a generic model class which matches the JSON object returned from the API
RxJava 1 returns a Subscription object not an Observable. RxJava 2 subscription returns void. That's why you are getting Error:(60, 27) error: incompatible types. You are getting the Disposable in the callback onSubscribe. If you need a reference to it, you can assign it to a class level member when the callback is invoked
Change returned object to Subscription
private Subscription subscription;
....
subscription = ApiClient.getInstance()
.getUsers(KEY, ADDRESS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<List<Users>>() {
#Override public void onCompleted() {
}
#Override public void onError(Throwable e) {
}
#Override public void onNext(List<Users> users) {
}
});
apiclient
public class ApiClient {
private static ApiClient instance;
private ApiQueryInterface apiqueryinterface;
private ApiClient() {
final Gson gson =
new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
final Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
apiqueryinterface = retrofit.create(ApiQueryInterface.class);
}
public static ApiClient getInstance() {
if (instance == null) {
instance = new ApiClient();
}
return instance;
}
public Observable<List<Users>> getUsers(#NonNull String key, #NonNull String address) {
return apiqueryinterface.getUsers(key, address);
}
}
interface
public interface ApiQueryInterface{
// Request method and URL specified in the annotation
// Callback for the parsed response is the last parameter
#GET("users")
Observable<<List<Users>> getUsers (
#Query("key") String key,
#Query("address") String address
);
I am getting response in a sequence:
"parameters": {
"parameter": {
"Data":"value"
}
},
"parameters":{
"parameter": [
{
"Data":"value"
},
{
"Data":"value"
},
]
},
Getting the error if I call List<Class> parameter:
Expected BEGIN_OBJECT but getting BEGIN_ARRAY
I need to parse parameter to get values
public class ApiClient {
public static final String BASE_URL ="http://.........";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
.addInterceptor(new ServiceGenerator("Content-Type","application/json")).build();
Gson gson = new GsonBuilder()
.setLenient()
.create();
if (retrofit==null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(client)
.build();
}
return retrofit;
}
}
public class ServiceGenerator implements Interceptor{
private String httpUsername;
private String httpPassword;
public ServiceGenerator(String httpUsername, String httpPassword) {
this.httpUsername = httpUsername;
this.httpPassword = httpPassword;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request newRequest = chain.request().newBuilder()
.addHeader("Authorization", getAuthorizationValue())
.build();
return chain.proceed(newRequest);
}
private String getAuthorizationValue() {
final String userAndPassword = httpUsername + ":" + httpPassword;
return "Basic " + Base64.encodeToString(userAndPassword.getBytes(), Base64.NO_WRAP);
}
}
#POST("OneWay.json")
Call<ApiResponse> sendOneWay(#Body Query data);
#SerializedName("FlightDetails")
public ApiResponse FlightDetails;
Now I called a Class ApiResponse
But How to call both
public ApiResponse FlightDetails; & public List FlightDetails;
This is just a very trivial issue that occurs often with APIs that have weird design choices. You just have to "align" both formats to a unified form: lists can cover both cases. So, all you have to implement is a type adapter that would check if such an alignment is necessary and use either the original type adapter if the value is a list, or wrap it up in a single element list.
For simplicity, consider the following JSON documents:
single.json
{
"virtual": {
"key-1": "value-1"
}
}
multiple.json
{
"virtual": [
{
"key-1": "value-1"
},
{
"key-2": "value-2"
}
]
}
Now define a mapping with the aligned field:
final class Response {
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
final List<Map<String, String>> virtual = null;
}
Note the JsonAnnotaion annotation: this is a way to tell Gson how the field must be read or written. The AlwaysListTypeAdapterFactory implementation might be as follows:
final class AlwaysListTypeAdapterFactory
implements TypeAdapterFactory {
// Always consider making constructors private
// + Gson can instantiate this factory itself
private AlwaysListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Not a list?
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
// Not something we can to deal with
return null;
}
// Now just return a special type adapter that could detect how to deal with objects
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(
(TypeAdapter<Object>) gson.getAdapter(TypeToken.get(getTypeParameter0(typeToken.getType()))),
(TypeAdapter<List<Object>>) gson.getAdapter(typeToken)
);
return castTypeAdapter;
}
// This is used to detect the list parameterization
private static Type getTypeParameter0(final Type type) {
if ( !(type instanceof ParameterizedType) ) {
// Is it a wildcard or raw type? Then we cannot determine the real parameterization
return Object.class;
}
// Or just resolve the actual E in List<E>
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
private static final class AlwaysListTypeAdapter<E>
extends TypeAdapter<List<E>> {
private final TypeAdapter<E> elementTypeAdapter;
private final TypeAdapter<List<E>> listTypeAdapter;
private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter, final TypeAdapter<List<E>> listTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
this.listTypeAdapter = listTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<E> value)
throws IOException {
listTypeAdapter.write(out, value);
}
#Override
public List<E> read(final JsonReader in)
throws IOException {
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
// If the next token is [, assume is a normal list, and just delegate the job to Gson internals
return listTypeAdapter.read(in);
case BEGIN_OBJECT:
case STRING:
case NUMBER:
case BOOLEAN:
case NULL:
// Any other value? Wrap it up ourselves, but use the element type adapter
// Despite Collections.singletonList() might be used, Gson returns mutable ArrayList instances, so we do...
final List<E> list = new ArrayList<>();
list.add(elementTypeAdapter.read(in));
return list;
case END_ARRAY:
case END_OBJECT:
case NAME:
case END_DOCUMENT:
// Something terrible here...
throw new MalformedJsonException("Unexpected token: " + token + " at " + in);
default:
// If someday Gson adds a new token
throw new AssertionError(token);
}
}
}
}
The test:
public static void main(final String... args)
throws IOException {
for ( final String resource : ImmutableList.of("single.json", "multiple.json") ) {
try ( final Reader reader = getPackageResourceReader(Q43634110.class, resource) ) {
final Response response = gson.fromJson(reader, Response.class);
System.out.println(resource);
System.out.println("\t" + response.virtual);
}
}
}
Output:
single.json
[{key-1=value-1}]
multiple.json
[{key-1=value-1}, {key-2=value-2}]
You could use this website to generate the java object for you
http://www.jsonschema2pojo.org/ just put the json response and choose Json for Source type and Gson for Annotation style.
and copy generated java class to your application and use it for the retrofit response .
The problem which you have here is that for the same json field you have different types. So the first time you are getting a JSON object and the second time a JSON array and this obviously will crash as you strictly defined to be parsed as an array (List).
You need to handle this case dynamically by your side or ask by the API guys to fix the bad data structure which seems you are getting back (except if it's on purpose like that).
To understand better the JSON types read this http://www.json.org/