gson反序列化成data class时的坑

Posted XeonYu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gson反序列化成data class时的坑相关的知识,希望对你有一定的参考价值。

前言

android开发中,gson是很常用的用来处理json的三方库,它是由Google维护的,一直以来都比较稳定,至少在使用Java开发时是这样的。

但是,gson对Kotlin的data class的支持就不是很完善了,会有一些坑,下面我们来看一看


gson反序列化成data class的正常情况

在kotlin中我们使用data class来充当数据类,举个例子:

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

使用gson将json解析成对象也非常简单,如下

fun main() 
    val jsonStr = """"name":"喻志强","age":0"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")

然后我们就得到一个User对象,数据也是ok的。之前也写过一些关于使用gson时的小技巧和封装,感兴趣的可以看一下:
Gson使用的一些小技巧
以上情况是建立在json数据正常的情况下。但json数据不那么正常时,坑就产生了,我们来看一下


属性值不能为null,结果反序列化后出现了null值

还是上面的例子,data class如下

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

如果json数据中的name没有了,如下

fun main() 
    val jsonStr = """"age":0"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")

解析出来的对象如下

一眼看上去这没毛病啊,都没有name字段,那name的值可不就是null吗?

然后在写代码时如果你稍微不注意,写了类似对name操作的代码,例如

fun main() 
    val jsonStr = """name:null,age:18"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")
    val substring = user.name.substring(0, 1)
    /*获取name的第一个字符*/
    println("substring = $substring")

运行

可不就报错吗?name是null啊,但是仔细一想这不对啊,这显然跟kotlin的语法产生了冲突,我们都知道,在data class中如果一个属性的值可以为null,我们需要用?来标识,如下


那这样在写代码时如果对name进行操作时,IDE会提示我们加上问号,这样可以有效避免出现空指针异常

加上?号运行一下,确实不报错了。

但是我们data class中的name没有加问号时也会出现null值,这就是第一个坑,跟预期不符。
原因是gson使用的是java的反射去构建的对象,也就是说gson并不认识data class,也就不会去满足kotlin空安全的特性,这样一来很容易出现奇怪的空指针异常。

解决该问题的方式很简单,就是给name加个?就好了,我个人是很不喜欢用空安全这个问号的,写起来有点恶心了,每个地方都要加问号,虽然可以有效避免空指针异常,而且在一些业务场景下,某些字段是必须要有值的,本来就不能为null,如果为null说明数据是有异常的,在反序列化时直接抛异常就好了。
gson的坑就在于无法在反序列化时就告诉你数据异常,只有到用到这个字段的值时,才会出现NPE,这个时候就晚了…


什么?data class默认值没有生效?

有些场景下,我们希望数据有一些默认值,或者说我们为了解决上面那个null值的问题,想着给name一个默认值,期望如果json中没有name这个字段的话,就用name的默认值好了,如下:

data class User(
    val name: String="喻志强",
    val age: Int,
)

然后再运行看一下

fun main() 
    val jsonStr = """age:28"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")
    val substring = user.name.substring(0, 1)
    /*获取name的第一个字符*/
    println("substring = $substring")

依旧报错

可以发现默认值并没有生效,仍然是null,打个断点你就知道原因是gson在找User的无参构造时没有找到,最终通过UnsafeAllocator.create()直接创建对象,根本没有走构造,构造都没走,给的默认值自然就不生效了

那解决这个的问题也很简单,我们给每个属性都赋一个初始值,这样就会生成一个无参的构造函数了,也就不会出现这种问题了,来试一下

data class User(
    val name: String="喻志强",
    val age: Int=0,
)

再次运行

fun main() 
    val jsonStr = """age:18"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")
    val substring = user.name.substring(0, 1)
    println("substring = $substring")

恩,这样好像就ok了,这个也是我自己在日常开发中的方式,我习惯给每个属性一个默认值,写代码的时候好处理一些。
但是给上默认值就万无一失了吗?当然不是
再来看下下面的代码

fun main() 
    val jsonStr = """name:null,age:18"""
    val gson = GsonBuilder().create()
    val user = gson.fromJson(jsonStr, User::class.java)
    println("user = $user")
    val substring = user.name.substring(0, 1)
    println("substring = $substring")

我们已经给data class所有属性默认值了,但是当json中某个属性的值显式的为null时,null值还是会覆盖到原本的默认值,这就又掉进第一个坑了,声明的时候不能为null,写代码的时候也没异常提示,结果运行就会出现NPE。

像这种返回数据不规范的情况还真没啥特别好的办法处理,唯一好处理的办法就是跟后端撕逼叫后端改…


不过我还找到了更好的解决办法,自己写后端,学一学crud就足够了,不用求人,我就是这样…

好了,不装了,总结一下gson结合data class时可能产生的两个主要问题吧

  • 属性声明时值不能为null,结果反序列化后值为null,跟预期不符
  • 默认值可能不生效,可能被null覆盖

其实针对上面的问题也有一些解决办法,自行查一下吧,但是不太喜欢,因为没有从根源解决问题。

要想从根源解决,很简单,不要用gson了,换moshi或者jackson就可以了,他们都针对kotlin做了单独的处理,具体用法官方文档也都有,这里就直接放出github地址了。

moshi:https://github.com/square/moshi

Jackson:jackson-module-kotlin


如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!

以上是关于gson反序列化成data class时的坑的主要内容,如果未能解决你的问题,请参考以下文章

gson反序列化成data class时的坑

gson反序列化成data class时的坑

Android gson 反序列化成列表

对kotlin友好的现代 JSON 库 moshi 基本使用和实战

对kotlin友好的现代 JSON 库 moshi 基本使用和实战

对kotlin友好的现代 JSON 库 moshi 基本使用和实战