Reputation: 3869
When I call the API, depending on the parameters, the name of the same field in the JSON returned changes. In the example below, in one case the field "user" is named "userA" or "userB". I'm using Gson but I don't want to create an object to parse the root of the JSON as I'm only interested in the list of users and I want my call to return this list only.
{
"apiVersion":"42"
"usersA":[{
"name":"Foo",
"lastname":"Bar"
...
}
]
"otherData":"..."
}
or
{
"apiVersion":"42"
"usersB":[{
"name":"Foo",
"lastname":"Bar"
...
}
]
"otherData":"..."
}
I know that I could use a TypeAdapter but I want to use the same Retrofit client to do different calls and the JSON structure can be very different depending on the API end point.
How can I do that?
Upvotes: 2
Views: 780
Reputation: 21105
I know that I could use a TypeAdapter but I want to use the same Retrofit client to do different calls and the JSON structure can be very different depending on the API end point.
Well, you can do it, and it's even easier with Retrofit 2 rather than plain Gson.
For example
final class User {
final String name = null;
final String lastname = null;
}
interface IService {
@GET("/")
@Unwrap
Call<List<User>> getUsers();
}
Note the @Unwrap
annotation above. This is an optional custom annotation marking that the call response body should be "unwrapped":
@Retention(RUNTIME)
@Target(METHOD)
@interface Unwrap {
}
Now you can just create a Retrofit converter factory that would analyze the annotation. Of course, this cannot cover all the cases, but it's extensible and you can improve it:
final class UnwrappingGsonConverterFactory
extends Converter.Factory {
private final Gson gson;
private UnwrappingGsonConverterFactory(final Gson gson) {
this.gson = gson;
}
static Converter.Factory create(final Gson gson) {
return new UnwrappingGsonConverterFactory(gson);
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
if ( !needsUnwrapping(annotations) ) {
return super.responseBodyConverter(type, annotations, retrofit);
}
final TypeAdapter<?> typeAdapter = gson.getAdapter(TypeToken.get(type));
return new UnwrappingResponseConverter(typeAdapter);
}
private static boolean needsUnwrapping(final Annotation[] annotations) {
for ( final Annotation annotation : annotations ) {
if ( annotation instanceof Unwrap ) {
return true;
}
}
return false;
}
private static final class UnwrappingResponseConverter
implements Converter<ResponseBody, Object> {
private final TypeAdapter<?> typeAdapter;
private UnwrappingResponseConverter(final TypeAdapter<?> typeAdapter) {
this.typeAdapter = typeAdapter;
}
@Override
public Object convert(final ResponseBody responseBody)
throws IOException {
try ( final JsonReader jsonReader = new JsonReader(responseBody.charStream()) ) {
// Checking if the JSON document current value is null
final JsonToken token = jsonReader.peek();
if ( token == JsonToken.NULL ) {
return null;
}
// If it's an object, expect `{`
jsonReader.beginObject();
Object value = null;
// And iterate over all properties
while ( jsonReader.hasNext() ) {
final String name = jsonReader.nextName();
// I'm assuming apiVersion and otherData should be skipped
switch ( name ) {
case "apiVersion":
case "otherData":
jsonReader.skipValue();
break;
// But any other is supposed to contain the required value (or null)
default:
value = typeAdapter.read(jsonReader);
break;
}
}
// Consume the object end `}`
jsonReader.endObject();
return value;
} finally {
responseBody.close();
}
}
}
}
I've tested it with the following code:
for ( final String filename : ImmutableList.of("usersA.json", "usersB.json") ) {
// Mocking the HTTP client to return a JSON document always
final OkHttpClient client = new Builder()
.addInterceptor(staticResponse(Q43921751.class, filename))
.build();
// Note the order of converter factories
final Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://whatever")
.client(client)
.addConverterFactory(UnwrappingGsonConverterFactory.create(gson))
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
final IService service = retrofit.create(IService.class);
service.getUsers()
.execute()
.body()
.forEach(user -> System.out.println(user.name + " " + user.lastname));
}
Output:
Foo Bar
Foo Bar
Upvotes: 2
Reputation: 566
You don't need to create special ResponseApi
class with different fields for your purposes. You can just create TypeAdapter
targeted for List<User>
class.
Like this:
Create TypeAdapter
class, e.g.
public class GsonUsersDeserializer extends TypeAdapter<List<User>> {
@Override
public void write(JsonWriter out, List<User> value) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public List<User> read(JsonReader in) throws IOException {
List<User> users = new ArrayList<>();
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case "usersA":
case "usersB":
// Try to read all users
in.beginArray();
while (in.hasNext()) {
users.add(readUser(in));
}
in.endArray();
break;
default:
// Just skip all other data in deserializer
in.skipValue();
break;
}
}
in.endObject();
return users;
}
private User readUser(JsonReader in) throws IOException {
User user = new User();
in.beginObject();
while (in.hasNext()) {
switch (in.nextName()) {
case "name":
user.setName(in.nextString());
break;
// ...
default:
in.skipValue();
break;
}
}
in.endObject();
return user;
}
}
When you create your Retrofit
client:
Retrofit provideRetrofit(OkHttpClient okHttpClien) {
// Create type targeted for list of users.
Type userListType = new TypeToken<List<User>>() {}.getType();
// Create Gson object for converter factory
Gson gson = new GsonBuilder()
.registerTypeAdapter(userListType, new GsonUsersDeserializer())
.create();
// Create Retrofit object
return new Retrofit.Builder()
.baseUrl(Config.BASE_SERVER_URL)
.addConverterFactory(GsonConverterFactory.create(gson ))
.client(okHttpClient)
.build();
}
And in your API interface:
public interface MyApi {
@GET("myUsersApiCall")
Call<List<User>> getUsers();
}
Upvotes: 0
Reputation: 146
Maybe it's a very stupid answer, but if u need only names/last names and don't want to deal with TypeAdapter for some reason AND you don't care about speed, you can ...
just go through the response body and using String Utils just scrape names and create a list.add(new User(String first, String last))
Kinda...
Upvotes: 0