Eselfar
Eselfar

Reputation: 3869

Parsing Retrofit2 result using Gson with different JSON structures

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

Answers (4)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

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

Pavel Strelchenko
Pavel Strelchenko

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:

  1. 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;
        }
    
    }
    
  2. 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();
    }
    
  3. And in your API interface:

    public interface MyApi {
    
        @GET("myUsersApiCall")
        Call<List<User>> getUsers();
    
    }
    

Upvotes: 0

Max
Max

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

Sunny
Sunny

Reputation: 137

Try to use alternate parameter in SerializedName annotation, like this:

@SerializedName(value="usersA", alternate={"usersB"}) User user;

More details are here.

Upvotes: -1

Related Questions