Parsing Retrofit2 result using Gson with different JSON structures

混江龙づ霸主 提交于 2019-12-24 04:26:17

问题


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?


回答1:


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




回答2:


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...




回答3:


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();
    
    }
    



回答4:


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

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

More details are here.



来源:https://stackoverflow.com/questions/43921751/parsing-retrofit2-result-using-gson-with-different-json-structures

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