【问题标题】:Parsing Retrofit2 result using Gson with different JSON structures使用具有不同 JSON 结构的 Gson 解析 Retrofit2 结果
【发布时间】:2017-10-10 19:55:38
【问题描述】:

当我调用 API 时,根据参数,返回的 JSON 中相同字段的名称会发生​​变化。在下面的示例中,在一种情况下,字段“user”被命名为“userA”或“userB”。 我正在使用 Gson,但我不想创建一个对象来解析 JSON 的根,因为我只对用户列表感兴趣并且我希望我的调用只返回这个列表。

{
    "apiVersion":"42"
    "usersA":[{
        "name":"Foo",
        "lastname":"Bar"
        ...
    }
    ]
    "otherData":"..."
}

{
    "apiVersion":"42"
    "usersB":[{
        "name":"Foo",
        "lastname":"Bar"
        ...
    }
    ]
    "otherData":"..."
}

我知道我可以使用 TypeAdapter,但我想使用同一个 Retrofit 客户端来执行不同的调用,并且 JSON 结构可能会因 API 端点而有很大不同。

我该怎么做?

【问题讨论】:

  • 您知道这个“用户”字段的所有可能的名称吗?如果是这样,您可以@SerializedName 中使用alternate 参数。如果您不了解它们,但知道其他领域,我建议您使用TypeAdapter 进行此 API 调用。但我真的不明白,为什么你认为TypeAdapter 会让你改变你的Retrofit 客户。
  • 不,我不知道所有可能的名字。我不太确定如何实现适配器。正如我所说,我只想从响应中获取用户列表,所以我想防止创建不必要的类,例如包含“apiVersion”的“ResponseAPI”以及所有其他不必要的类。
  • 您应始终避免将个人信息放在应用程序的代码或打包文件中。将任何可能的名称放在 @SerializedName 中会产生一个非常糟糕的应用程序。

标签: android json gson retrofit2


【解决方案1】:

我知道我可以使用 TypeAdapter,但我想使用同一个 Retrofit 客户端来执行不同的调用,并且 JSON 结构可能会因 API 端点而有很大不同。

嗯,你可以做到,而且使用 Retrofit 2 比普通的 Gson 更容易。

例如

final class User {

    final String name = null;
    final String lastname = null;

}
interface IService {

    @GET("/")
    @Unwrap
    Call<List<User>> getUsers();

}

注意上面的@Unwrap 注释。这是一个可选的自定义注释,标记调用响应正文应“展开”:

@Retention(RUNTIME)
@Target(METHOD)
@interface Unwrap {
}

现在您可以创建一个 Retrofit 转换器工厂来分析注释。当然,这并不能涵盖所有情况,但它是可扩展的,您可以对其进行改进:

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

    }

}

我已经用以下代码对其进行了测试:

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

输出:

美食吧
富吧

【讨论】:

    【解决方案2】:

    也许这是一个非常愚蠢的答案,但是如果您只需要姓名/姓氏并且由于某种原因不想处理 TypeAdapter 并且您不关心速度,您可以...
    只需浏览响应正文并使用 String Utils 只需抓取名称并创建一个 list.add(new User(String first, String last))
    有点...

    【讨论】:

      【解决方案3】:

      您不需要为您的目的创建具有不同字段的特殊ResponseApi 类。您可以为List&lt;User&gt; 类创建TypeAdapter。 像这样:

      1. 创建TypeAdapter 类,例如

        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. 当您创建 Retrofit 客户端时:

        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. 在您的 API 接口中:

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

      【讨论】:

        【解决方案4】:

        尝试在SerializedName注解中使用alternate参数,像这样:

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

        更多详情请联系here

        【讨论】:

        • 是的,但我无法预测该字段的名称
        • 将个人数据放入代码中并不是正确的做法。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-10-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多