采用Gson解析含有多种JsonObject的复杂json

本文对应的项目是MultiTypeJsonParser ,项目地址 https://github.com/sososeen09/MultiTypeJsonParser

0 前奏

使用 Gson 去解析 json 应该是很常见的,大部分的情况下我们只要创建一个 Gson 对象,然后根据 json 和对应的 Java 类去解析就可以了。

Gson gson = new Gson();
Person person = gson.form(json,Person.class);

但是对于比较复杂的 json,比如下面这种, attributes 对应的 jsonObject 中的字段是完全不一样的,这个时候再简单的用上面的方法就解析不了了。

{
    "total": 2,
    "list": [
        {
            "type": "address",
            "attributes": {
                "street": "NanJing Road",
                "city": "ShangHai",
                "country": "China"
            }
        },
        {
            "type": "name",
            "attributes": {
                "first-name": "Su",
                "last-name": "Tu"
            }
        }
    ]
}

当然了,我们说一步到位的方式解决不了,但用一点笨方法还是可以的。比如先手动解析拿到 attributes 对应的 jsonObject,根据与它同级 type 对应的 value 就可以判断这一段 jsonObject 对应的 Java 类是哪个,最后就采用 gson.from() 方法解析出 attributes 对应的 Java 对象。


ListInfoWithType listInfoWithType = new ListInfoWithType();

//创建 org.json 包下的 JSONObject 对象
JSONObject jsonObject = new JSONObject(TestJson.TEST_JSON_1);
int total = jsonObject.getInt("total");

//创建 org.json 包下的 JSONArray 对象
JSONArray jsonArray = jsonObject.getJSONArray("list");
Gson gson = new Gson();
List<AttributeWithType> list = new ArrayList<>();

//遍历
for (int i = 0; i < jsonArray.length(); i++) {
    JSONObject innerJsonObject = jsonArray.getJSONObject(i);
    Class<? extends Attribute> clazz;
    String type = innerJsonObject.getString("type");
    if (TextUtils.equals(type, "address")) {
        clazz = AddressAttribute.class;
    } else if (TextUtils.equals(type, "name")) {
        clazz = NameAttribute.class;
    } else {
        //有未知的类型就跳过
        continue;
    }
    AttributeWithType attributeWithType = new AttributeWithType();

//采用Gson解析
    Attribute attribute = gson.fromJson(innerJsonObject.getString("attributes"), clazz);
    attributeWithType.setType(type);
    attributeWithType.setAttributes(attribute);
    list.add(attributeWithType);
}

listInfoWithType.setTotal(total);
listInfoWithType.setList(list);

虽然这样能实现整个 json 的反序列化,但是这种方式比较麻烦,而且一点也不优雅,如果项目中存在很多这样的情况,就会做很多重复的体力劳动。
如何更优雅、更通用的解决这类问题,在网上没有找到答案,只好去深入研究一下Gson了。带着这样的目的,翻看了Gson的文档,发现了一句话

Gson can work with arbitrary Java objects including pre-existing objects that you do not have source code of.

这句话说 Gson 可以处理任意的 Java 对象。那么对于上面讲的那种反序列化情况来讲, Gson 应该也能做到。通过研究 Gson 的文档,发现可以通过自定义JsonDeserializer的方式来实现解析这种 jsonObject 类型不同的情况。

我们知道,大部分情况下 Gson 是通过直接 new 出来的方式来创建,不过也可以采用 GsonBuilder 这个类去生成 Gson。

  Gson gson = new GsonBuilder()
   .registerTypeAdapter(Id.class, new IdTypeAdapter())
   .enableComplexMapKeySerialization()
   .serializeNulls()
   .setDateFormat(DateFormat.LONG)
   .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)
   .setPrettyPrinting()
   .setVersion(1.0)
   .create();

GsonBuilder 通过 registerTypeAdapter()方法,对目标类进行注册。当序列化或者反序列化目标类的时候就会调用我们注册的typeAdapter, 这样就实现了人工干预 Gson 的序列化和反序列化过程。

GsonBuilder 的 registerTypeAdapte() 方法的第二个参数是 Object 类型,也就意味着我们可以注册多种类型的 typeAdapter,目前支持的类型有 JsonSerializer、JsonDeserializer、InstanceCreator、TypeAdapter。

  public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) 

经过一番捣鼓,写了一个工具类,对于上面的那个复杂 json,用了不到10行代码就搞定,而且比较优雅和通用。

MultiTypeJsonParser<Attribute> multiTypeJsonParser = new MultiTypeJsonParser.Builder<Attribute>()
        .registerTypeElementName("type")
        .registerTargetClass(Attribute.class)
        .registerTargetUpperLevelClass(AttributeWithType.class)
        .registerTypeElementValueWithClassType("address", AddressAttribute.class)
        .registerTypeElementValueWithClassType("name", NameAttribute.class)
        .build();

ListInfoWithType listInfoWithType = multiTypeJsonParser.fromJson(TestJson.TEST_JSON_1, ListInfoWithType.class);

本文就简单分析一下如何通过自定义 JsonDeserializer 来实现一个通用的工具类用于解析复杂类型 json。对于以后碰到相似问题,这种处理方法可以提供一种解决问题的思路。具体的代码和实例,可以查看项目。如果对您的思路有一些启发,欢迎交流和Star。

1 JsonDeserializer介绍

JsonDeserializer 是一个接口,使用的时候需要实现这个接口并在 GsonBuilder 中对具体的类型去注册。当反序列化到对应的类的时候就会调用这个自定义 JsonDeserializer 的 deserialize() 方法。下面对这个方法的几个参数做一下解释,以便于更好的理解Gson解析的过程。

public interface JsonDeserializer<T> {
  public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException;
}

1.1 JsonElement

JsonElement代表 在 Gson 中的代表一个元素。它是一个抽象类,有4个子类:JsonObject、JsonArray、JsonPrimitive、JsonNull。
1.JsonObject 表示的是包含name-value型的 json 字符串,其中 name 是字符串,而 value 可以是其它类型的 JsonElement 元素。在json中用 “{}” 包裹起来的一个整体就是JsonObject。例如

// "attributes" 是name,后面跟着的{}内容是它对应的value,而这个value就是一个JsonObject
  "attributes": {
                  "first-name": "Su",
                  "last-name": "Tu"
                 }

2.JsonArray 这个类在 Gson 中代表一个数组类型,一个数组就是JsonElement的集合,这个集合中每一个类型都可能不同。这是一个有序的集合,意味着元素的添加顺序是被维持着的。上面例子中list对应的 “[]” 包裹起来的json就是JsonArray。

3.**JsonPrimitive ** 这个可以认为是json中的原始类型的值,包含Java的8个基本类型和它们对应的包装类型,也包含 String 类型。比如上面 "first-name" 对应的 "Su" 就是一个 String 类型的 JsonPrimitive 。

4.JsonNull 通过名字也可以猜到,这个代表的是 null 值。

1.2 Type

Type是Java中的所有类型的顶层接口,它的子类有 GenericArrayType、ParameterizedType、TypeVariable、WildcardType,这个都是在java.lang.reflect包下面的类。另外,我们最熟悉的一个类 Class 也实现了 Type 接口。

一般来讲,调用 GsonBuilder 的 registerTypeAdapter() 去注册,第一个参数使用 Class 类型就可以了。

1.3 JsonDeserializationContext

这个类是在反序列过程中,由其它类调用我们自定义的 JsonDeserialization 的 deserialize() 方法时传递过来的,在 Gson 中它唯一的一个实现是TreeTypeAdapter 中的一个私有的内部类 GsonContextImpl 。可以在自定义的 JsonDeserializer 的 deserialize() 中去调用 JsonDeserializationContext 的 deserialize() 方法去获得一个对象。

但是要记住,如果传递到 JsonDeserializationContext 中的 json 与 JsonDeserializer 中的 json 一样的话,可能会导致死循环调用。

2 思路分析

2.1 创建JavaBean

还是以最上面的那个 json 进行分析,在 list 对应 JsonArray ,其中的两个 JsonObject 中,attributes 对应的 JsonObject 字段完全不一样,但是为了统一,在写 JavaBean 的时候可以给它们设置一个共同的父类,尽管它是空的。

public class Attribute {
      ...
}

public class AddressAttribute extends Attribute {
    private String street;
    private String city;
    private String country;
... 省略get/set
}

public class NameAttribute extends Attribute {
    @SerializedName("first-name")
    private String firstname;
    @SerializedName("last-name")
    private String lastname;
...省略get/set
}

设置 Attribute 这个 SuperClass 只是为了在 GsonBuilder 去注册,当具体解析的时候我们会根据
type 对应的类型去找到对应的Class。

 gsonBuilder.registerTypeAdapter(Attribute.class, new AttributeJsonDeserializer());

到了这里我们就应该想到,type 对应的 value 肯定是要与具体的 JavaBean 对应起来的。比如在这里就是

"address"——AddressAttribute.class
"name"——NameAttribute.class

如果 type 是 "address" ,那么我们就可以用 gson 去拿 AddressAttribute.class 和对应的 json 去解析。

Attribute attribute = gson.form(addressJson,AddressAttribute.class);

2.2 如何把 json 准确的转为对应的 JavaBean

我们注册的是父类 Attribute ,当反序列化需要解析 Attribute 的时候就会把对应的 json 作为参数回调自定义的 JsonDeserializer 。我们就可以在下面这个方法中写自己的逻辑得到我们需要的 Attribute 对象了。

 public Attribute deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)

但是细心的朋友应该会发现了,这个时候传递的 json 有可能是这样的

{
   "street": "NanJing Road",
   "city": "ShangHai",
   "country": "China"
}

也有可能是这样的

{
   "first-name": "Su",
   "last-name": "Tu"
}

我们怎么知道该解析成 AddressAttribute 还是 NameAttribute ???

我们想想,具体解析成哪个,我们肯定是需要知道 type 对应的 value 。而这个 type 是与 attributes 同级的字段,照着刚才这样肯定是没希望拿到这个 value 的。

我们再想想,能够知道这个 type 对应的 value 是什么的肯定是 attributes 上一层级的 json 。

{
   "type": "name",
   "attributes": {
                          ...
                 }  
}

那么我们可不可以在 GsonBuilder 中再去注册一个 typeAdapter 来解析这个外层的 json 呢?当然可以。

 gsonBuilder.registerTypeAdapter(AttributeWithType.class, new AttributeWithTypeJsonDeserializer());

这个 AttributeWithType 就是外层的 json 对应的 JavaBean

public class AttributeWithType {
    private String type;
    private Attribute attributes;
     ...
}

在反序列化 AttributeWithType 这个类的时候,我们可以获得这个 type 对应的 value,然后把这个 value 传递给里层的 Attribute 对应的 JsonDeserializer。这样就可以根据 value 是 “address” 或者 “name” 去对 AddresAttribute 或者 NameAttribute 进行反序列化了。

2.3 有一个坑

前面那我们讲过,调用 JsonDeserializationContext 的方法应该注意死循环。在具体的实践中,我虽然没有调用 JsonDeserializationContext 的方法,但是依然出现了死循环的情况。就是因为我是这么用的。

 AttributeWithType attributeWithType = gson.fromJson(json, AttributeWithType.class);

乍一看没什么问题啊,问题就出在这个 gson 身上。这个 gson 是已经注册过解析 AttributeWithType 的 GsonBuilder 创建的。 gson.fromJson() 方法中的 json 是 AttributeWithType 对应的反序列化的 json,gson.fromJson() 内部会再次调用 AttributeWithType 对应的 JsonDeserializer 中的 deserialize() 方法,从而导致死循环。

避免死循环的方式就是用GsonBuilder新建一个 gson ,这个GsonBuilder不再注册 AttributeWithType ,而只去注册 Attribute 去解析。

3 为了更好更通用

1.在项目中,可能还会存在另一种格式的json,外部没有单独的type元素,而是与其它的元素放在同一个JsonObject中。这样的格式更省事,不需要注册外层的typeAdaper即可。

{
    "total": 2,
    "list": [
        {
            "type": "address",
            "street": "NanJing Road",
            "city": "ShangHai",
            "country": "China"
        },
        {
            "type": "name",
            "first-name": "Su",
            "last-name": "Tu"
        }
    ]
}

MultiTypeJsonParser<Attribute> multiTypeJsonParser = new MultiTypeJsonParser.Builder<Attribute>()
        .registerTypeElementName("type")
        .registerTargetClass(Attribute.class)
// 如果所要解析的 jsonObejct 中已经含有能够表示自身类型的字段,不需要注册外层 Type,这样更省事
//        .registerTargetUpperLevelClass(AttributeWithType.class)
        .registerTypeElementValueWithClassType("address", AddressAttribute.class)
        .registerTypeElementValueWithClassType("name", NameAttribute.class)
        .build();

ListInfoWithType listInfoWithType = multiTypeJsonParser.fromJson(TestJson.TEST_JSON_1, ListInfoWithType.class);

2.如果在解析过程中发现有些类型没有注册到 MultiTypeJsonParser 的 Builder 中,解析的时候碰到相应的 jsonObject 就直接返回null。比如下面这样的json中,"type" 对应的 "parents" 如果没有注册,那么反序列化的时候这个 json 所代表的对象就为 null 。

 {
        "type": "parents",
        "attributes": {
          "mather": "mi lan",
          "father": "lin ken"
        }
 }

在Android中我们反序列这样的 json 后一般会把得到的对象的设置到列表控件上,如果后端返回的 json 中包含之前未注册的类型,为了程序不至于 crash,需要对反序列化的 null 对象进行过滤,项目中提供了一个工具类 ListItemFilter 可以过滤集合中为 null 的元素。

4 结语

对于如何优雅的解析这种类型不同的 JsonObject ,刚开始我是缺少思路的,在网上也没有查到合适的文档。但是通过查看 Gson 的文档和源码,通过自己的理解和分析,逐步的完成了这个过程。我的一个感触就是,多去看看官方的使用文档应该比盲目去搜索解决方案更好。

代码是最好的文档,本文只简单介绍了一些实现思路,文中贴出的一些代码是为了讲述方便,与项目中的代码可能会有有些区别。具体的使用可以看项目中的例子。

如果有问题,欢迎提 issue 或留言,如果对您有所帮助,欢迎Star。

参考

Gson官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,783评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,360评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,942评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,507评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,324评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,299评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,685评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,358评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,652评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,704评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,465评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,318评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,711评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,991评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,265评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,661评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,864评论 2 335

推荐阅读更多精彩内容