Kotlin下的Gson解析Json数据

Kotlin作为Android的官方开发语言已经有一段时间了,在项目过程中总结了一些Kotlin下处理Json的经验,记录下来和大家分享。

Kotlin的数据类

Kotlin中,有一种只保存数据的类,称作数据类。关键字是class前加一个data。例如:

1
data class User(val name: String, val age: Int)

一切看起来很美好,接触过Kotlin的同学肯定知道,普通类下的var变量或val常量,都会有恼人的初始化提示,尤其是类的成员变量(var),如果不给予初始化赋值,那么就需要手动添加lateinit关键字,如果初始化null,那么类型就变成了xx?, 对于Bean类而言,这些都是累赘。Data类正好解决了我们的问题。但是Kotlin的Data类在搭配Gson使用时,真的那么美好吗?看下一个问题。

Data class的字段可否为空的问题

首先我们来看一个例子:

1
2
3
4
5
6
7
8
9
data class User(
val name: String,
val age: Int,
val data: List<String>
) {
override fun toString(): String {
return "User(name='$name', age=$age, data=$data)"
}
}

这里我们看到,name,age,data 我们都申明为非空,但是实际上,这样的申明是没有多大用处的,举个最简单的例子:

1
println(Gson().fromJson("", User::class.java))

输出结果:

1
User(name='null', age=0, data=null)

显然,除了Int类型的age之外,其它两个变量全都是null,这样Kotlin赋予的空指针检查也就无法使用了。

那么一种方式是我们把字段申明为可空,也就是:

1
2
3
4
5
data class User(
val name: String?,
val age: Int,
val data: List<String>?
)

这是一种思路,但很多时候我们希望能设置一个默认值,于是我们按照Java的写法,尝试字符串默认值为空;

1
2
3
4
5
data class User(
val name: String = "",
val age: Int,
val data: List<String>?
)

还是上面的例子,

1
println(Gson().fromJson("", User::class.java))

结果输出的是:

1
User(name='null', age=0, data=null)

居然没有生效,name原本的默认空值也被覆盖为null了, 这个问题网上也讨论了很多,其实是Gson这个库的处理方式导致的,在Gson的fromJson方法中,最终会尝试通过反射的方式,获取对象的无参构造函数去创建对象。简单来说,我们这里的User类,实际上他的构造函数有两个,分别是:

User(age: Int, data: List)
User(name: String, age: Int,data: List)

而显然,并没有无参的构造函数,而Gson这里的反射构造对象,会绕过构造函数,只会在堆中去分配一个对象实例。

解决方式目前来看,就是如果需要默认值,那么所有参数都设置默认值:

1
2
3
4
5
data class User(
val name: String = "",
val age: Int = 0,
val data: List<String>? = null
)

这样User相当于有一个User()的无参构造函数了,这样我们的初始化配置也会生效。

具体可以参考这篇博客:Gson 反序列化 Kotlin 数据类默认值失效

Json序列化空字符串如何当做null处理?

某些时候我们在将对象转成Json String的过程中,需要特别处理某些空字符串。例如拼接到url后面的参数,如果字符串为空,就不拼到参数当中。例如:

1
2
3
4
5
6
7
8
data class Params(
val p1: String = "",
val p2: String = "",
val p3: String? = null
)

val params = Params("p1", "", null)
println(Gson().toJson(params))

输出结果:

1
2
3
4
//预期
{"p1":"p1"}
//实际
{"p1":"p1","p2":""}

这里其实是可以通过自定义序列化的方式来解决的,也就是将空字符串当做null来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class EmptyStringAsNullTypeAdapter: JsonSerializer<String> {

override fun serialize(src: String?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
return if (TextUtils.isEmpty(src)) {
JsonNull.INSTANCE
} else {
JsonPrimitive(src.toString())
}
}
}

val paramsGson = GsonBuilder()
.registerTypeAdapter(String::class.java, EmptyStringAsNullTypeAdapter())
.create()
println(paramsGson.toJson(params))

如何统一过滤Array中的null元素?

数组里的null元素是一个很坑的事情,通常情况下,除非后台在数组中明确返回了null元素之外,是不太可能在数组中存在null的。例如:

1
2
3
4
5
6
7
8
data class User(
val data: List<String>? = null
)

val str = "{\"data\": [null, \"s1\"]}"
println(Gson().fromJson(str, User::class.java))

//输出 User(name='', age=0, data=[null, s1])

显然,在每个数组的声明中都申明为可空想想就是件麻烦的事,而且读取的时候额外的元素判空也是很恶心的一件事。

1
2
3
4
5
data class User(
val data1: List<String?>? = null,
val data2: List<String?>? = null,
val data3: List<String?>? = null
)

当然我们尽量希望后台不要传null在数组中,但有的时候无法避免这种情况,该怎么处理呢?针对这种反序列化的情况,我们需要自定义JsonDeserializer

1
2
3
4
5
6
class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> {

override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List<T> {
//替换 Gson().fromJson(json, typeOff)
}
}

接着问题来了,我们来看一下下面这个做法,能否满足我们的需求吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> {

@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List<T> {
val jsonArray = JsonArray()
for (jsonElement in json.asJsonArray) {
if (jsonElement.isJsonNull) {
continue
}
jsonArray.add(jsonElement)
}

return Gson().fromJson(jsonArray, typeOfT)
}
}

这里的做法是将当前JsonArray中的null元素移除,但其实这样做是不够的,举个例子大家就明白了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data class A(
val list1: List<String>,
val b: B,
val list2: List<C>
)

data class B(
val values: List<Int>
)

data class C(
val values: List<Int>
)

val gson = GsonBuilder()
.registerTypeAdapter(List::class.java, RemoveNullListDeserializer<Any>())
.create()
val a = gson.fromGson(jsonStr, A::class.java)

我们这里注册的反序列化的方法,实际上只针对A.list1、A.list2和A.b.values三个元素生效,而A.list2.values是没办法接管到的。原因在于C的values的反序列化在注册的时候被更高层的List覆盖了。所以我这里的处理方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class RemoveNullListDeserializer<T> : JsonDeserializer<List<T>> {

@Throws(JsonParseException::class)
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List<T> {
removeNullEleInArray(json)
return Gson().fromJson(json, typeOfT)
}

private fun removeNullEleInArray(json: JsonElement) {
if (json.isJsonArray) {
val jsonArray = json.asJsonArray
var i = 0
while (i < jsonArray.size()) {
val ele = jsonArray.get(i)
if (ele.isJsonNull) {
jsonArray.remove(i)
continue
}
removeNullEleInArray(ele)
i++
}
} else if (json.isJsonObject) {
val jsonObject = json.asJsonObject
jsonObject.entrySet()
.filter {
it.value.isJsonArray || it.value.isJsonObject
}
.forEach {
removeNullEleInArray(it.value)
}
}
}
}

在Gson中,抽象类JsonElement一共有四种类型,分别是JsonObject, JsonArray, JsonPrimitive, JsonNull.
可能存在数组的地方只可能是JsonArray或JsonObject。这里用了简单的递归,在反序列化数据之前,手动遍历所有可能存在的List,删除null元素。

总结

Kotlin的数据类为我们定义数据类型的类提供了便利的同时,搭配Gson使用的时候,因为Gson采用了反射的方法获取默认的无参构造函数创建对象,如果没有无参构造函数的话,则通过反射直接绕过了构造函数创建对象,所以如果想让默认值生效,则必须提供无参的构造函数。而提供无参的构造函数的本身其实在开发上增加了负担。