【转】采用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官方文档

上一篇:MongoDB学习笔记~地图坐标的支持与附近点的查找


下一篇:Chrome内置的断网Javascript 小游戏脚本示范