Gson同字段不同类型数据解析总结

前言
最近使用Retrofit进行网络请求时,自带Gson解析json时遇到一个问题,返回的json数据中某个字段可能为jsonArray,也可能是jsonObject,也有可能为空(即同一个字段,返回可能是对象,数组,null)。
错误回顾
测试数据:
Gson同字段不同类型数据解析总结
文章图片

如果返回的数据类型有两种以上,但你定义json的实体类bean属性类型时,可能只使用了jsonObject或者jsonArray,网络请求时会报如下错误:

Caused by: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 4 column 514 path $.CanLoanBook at com.google.gson.stream.JsonReader.beginArray(JsonReader.java:350) at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:80) at com.google.gson.internal.bind.CollectionTypeAdapterFactory$Adapter.read(CollectionTypeAdapterFactory.java:61) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129) at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:22

【Gson同字段不同类型数据解析总结】上面出错原因是字段CanLoanBook使用了数组进行映射,但CanLoanBook实际是对象。根据上面错误提示可能你会这样做:
把实体类的字段从原来的
private List CanLoanBook;

改为如下:

private CanLoanBookBean CanLoanBook;

上面确实临时 解决了问题,但当后端CanLoanBook返回是数组时,上面还是会报错,不妨自己试一下。
另外,网上也有一些方案通过自定义Gson序列化器或者解析异常时,使用另一种是实体类处理,这两种方式要么处理麻烦,要么产生多余的类。
解决方案
1、与后端协商,规范数据格式,保证返回字段类型不变
这种情况虽然处理起来最省事,得到期望结果,但是后端的大佬可能不会随便改动数据接口,让你想办法解决。严格来说,这种字段数据类型变化是不规范化的。
2、把数据映射为ResponseBody再解析
这种情况要先把ResponseBody转成字符串,再采用对应框架,逐个字段解析封装到对应实体,然后需要对特殊字段进行数据类型判断。这种方案虽然可取,但解析麻烦,费时,显然不给力。
下面顺便介绍下两种方法怎么对json数据字段类型判断:
1.使用Android自带的JSONTokener解析
try { JSONObject jsonObject = JSONObject(jsonStr); String canLoanBook = jsonObject.getString("CanLoanBook"); Object obj = new JSONTokener(canLoanBook).nextValue(); if (obj instanceof JSONArray) { JSONArray jsonArray = (JSONArray) obj; for (int k = 0; k < jsonArray.length(); k++) { JSONObject parameterObject = jsonArray.getJSONObject(k); //开始逐个字段解析,封装到对应bean,省略 } } else if (obj instanceof JSONObject) { JSONObject jObj = (JSONObject) obj; //开始逐个字段解析,封装到对应bean,省略 } } catch (JSONException e) { e.printStackTrace(); }

2.使用Gson框架解析
Gson gson = new Gson(); JsonParser parser = new JsonParser(); JsonElement element = parser.parse(jsonStr); if (element.isJsonObject()) {//假设最外层是object // 把JsonElement对象转换成JsonObject JsonObject JsonObject = element.getAsJsonObject(); JsonElement jsonElement = JsonObject.get("CanLoanBook"); if (jsonElement.isJsonObject()) { CanLoanBookBean canLoanBookBean = gson.fromJson(jsonElement, CanLoanBookBean.class); } else if (jsonElement.isJsonArray()) { Type type = new TypeToken>() {}.getType(); // 把JsonElement对象转换成JsonArray List list = gson.fromJson(jsonElement, type); } }

3、把实体类特殊字段的数据类型改为Object再转换
也就是说不管json字段是对象还是数组,统一使用Object接收数据,这样不会出现异常。
/** * 1.不管是jsonArray还是jsonObject,统一用Object接收 * 2.使用Gson对CanLoanBook字段统一换成List,反射赋值CanLoanBook * 3.最后对CanLoanBook强转成对应类型 */ private Object CanLoanBook;

这种方式需要再用gson对Object对象进行手动解析,可以参考上面方案2的第2种解析方式。
附上转换方法:
/** * 转换和赋值 * * @param sourceObj原数据对象 * @param methodName需要赋值原数据对象的方法名 * @param filedObjValue 需要处理的字段对象值 * @param filedMapClass 处理的字段对象映射Class */ public static void performTransformWithEvaluation(Object sourceObj, String methodName, Object filedObjValue, Class filedMapClass) { List listObjects = performTransform(filedObjValue, filedMapClass); Class clazz = sourceObj.getClass(); Method method = null; try { method = clazz.getMethod(methodName, Object.class); method.invoke(sourceObj, listObjects); } catch (Exception e) { e.printStackTrace(); } }/** * 统一转换成List输出,对于基本类型,取出集合中的值即可 * * @param filedObjValue 需要处理的字段对象值 * @param filedMapClass 处理的字段对象映射Class * @return */ public static List performTransform(Object filedObjValue, Class filedMapClass) { List beanList = new ArrayList(); Gson gson = new Gson(); String jsonStr = gson.toJson(filedObjValue); JsonParser parser = new JsonParser(); JsonElement element = parser.parse(jsonStr); if (element.isJsonObject()) { // 把JsonElement对象转换成JsonObject T t = (T) gson.fromJson(element, filedMapClass); beanList.add(t); } else if (element.isJsonArray()) { //下面会导致T为LinkedTreeMap,说明Gson解析时不支持泛型 //Type type = new TypeToken>() {}.getType(); //// 把JsonElement对象转换成JsonArray //List list = gson.fromJson(element, type); List list = jsonToList(element,filedMapClass); beanList.addAll(list); } else if (element.isJsonPrimitive()) { T t = (T) gson.fromJson(element, filedMapClass); beanList.add(t); } else { // element.isJsonNull() return null; } return beanList; }/** * 通过json字符串转List * @param json * @param clazz * @param * @return */ public static List jsonToList(String json, Class clazz) { Type type = new ParameterizedTypeImpl(clazz); List list = new Gson().fromJson(json, type); return list; }/** * 通过element转List * @param element * @param clazz * @param * @return */ public static List jsonToList(JsonElement element, Class clazz) { Type type = new ParameterizedTypeImpl(clazz); List list = new Gson().fromJson(element, type); return list; }/** * 自定义ParameterizedType */ private static class ParameterizedTypeImpl implements ParameterizedType { Class clazz; public ParameterizedTypeImpl(Class clz) { clazz = clz; }/** * 返回实际类型组成的数据,比如Map map的类型是:java.lang.String、java.lang.Long * @return */ @Override public Type[] getActualTypeArguments() { return new Type[] { clazz }; }/** * 返回原生类型,比如Map map的原生类型为java.util.Map * @return */ @Override public Type getRawType() { return List.class; }/** * 返回 Type 对象,表示此类型是其成员之一的类型,比如Map.Entry map的OwnerType为java.util.Map * @return */ @Override public Type getOwnerType() { return null; } }
关于参数化类型ParameterizedType(除此之外,还有TypeVariable、GenericArrayType、WildcardType,其父接口Type是Java 编程语言中所有类型的公共高级接口) 的用法这里就不多说,在Android开发中很常见,比如:MVP模式中BaseActivity,经常指定一个泛型P(Presenter),让子类继承BaseActivity并制定具体的Presenter;RecyclerView.Adapter等等。
基本使用示例:
BookBean bookBean = gson.fromJson(jsonStr, BookBean.class); performTransformWithEvaluation(bookBean, "setCanLoanBook", bookBean.getCanLoanBook(), CanLoanBookBean.class); ListcanLoanBookBeans=(List) bookBean.getCanLoanBook(); System.out.println("canLoanBookBeans:" + canLoanBookBeans);

在Retrofit使用示例:
RetrofitUtils.getInstance().getService().getBookDetail(v_recno, v_curtable, time).map((Function) bookDetail -> {JsonUtils.performTransformWithEvaluation(bookDetail,"setCanLoanBook",bookDetail.getCanLoanBook(),BookDetail.CanLoanBookBean.class); return bookDetail; }) .compose(RxSchedulers.applySchedulers()) .subscribe((Consumer) bookDetail -> {}, (Consumer) throwable -> {});

上面还可以再优化一下,自定义一个Gson反序列化器,把上述方法放进序列化器中,这样网络请求后,不需要对数据进行map转换。
方案对比
方案1:是最省事的,不建议后端数据接口同字段返回不同类型数据(适合字段数据类型固定,正常情况都是这种数据)
方案2:解析费时费力,完全手动解析(适合大量字段数据类型变化的情况)
方案3(建议):不用改Retrofit现有Gson转换器,只需要对特殊字段处理即可(适合少量字段数据类型变化的情况,如果都变化,这样的json数据存在意义不大)
总结
本次就是开发过程中遇到的问题,只要网络请求出现类似的问题:同字段返回不同类型数据,解析框架出现IllegalStateException,都可以用上述方案3解决。

    推荐阅读