Gson反序列化时对于null的处理方案

在平常开发的过程中,使用json传递数据已经非常常用了,在我做安卓程序开发的时候,从接口拿到的数据都是json格式的,通常我们会选择使用gson库进行解析,使用起来确实很方便。但有的时候服务端会返回一些带null的数据,这样我们在解析之后属性的值就变成了null,这样非常容易导致空指针异常,即使是定义模型的时候规定字段不能为空或为其设置一个默认值都无法解决。在多次与后台同学沟通无果后只能自己想办法把这个坑填上顺便把后台同学杀掉。

为了解决这个问题,有两个方向可以考虑:

  1. 在解析的时候忽略空值
  2. 将数据中的空值去掉

第一种方案也有别人做过,核心逻辑就是定义一些TypeAdapter,在反序列化时对当前字段为空的情况进行处理,但这种方案有两个主要问题:

  1. 如果要统一封装,只能针对一些基础类型定义对应的适配器,如果是用户自己定义的一些数据模型,就需要用户自己定义对应的适配器。
  2. 无法保留数据模型中的默认值,通常我们见到的字符串适配器只能在字段为空时返回空字符串,模型中的默认值就会被覆盖。

第二种方案相当于是在解析json字符串之前把所有的null都去掉,之后再进行反序列化的时候就没有空值了。没有了空字段的干扰,正常的反序列化流程就不会再给字段赋值为空指针了。确定了解决方案之后就可以开始施工了。

创建JsonDeserializer

我们定义了一个JsonDeserializer用于在反序列化的时候进行数据处理,为了能够对所有模型的解析进行处理,把泛型指定为了Any,在GsonAdapter中通过递归调用searchInObject方法和searchInArray方法来遍历全部数据。JsonElementisJsonNull方法可以用来判断空指针,我们就通过这个方法来找到全部的空指针并将其对应的字段删除。通过预处理流程,我们已经得到了没有空指针的数据,最后直接调用原始的gson进行解析即可。

private class GsonAdapter(private val gson: Gson) : JsonDeserializer<Any> {
    override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Any {
        if (json?.isJsonObject == true) {
            val obj = json.asJsonObject
            searchInObject(obj)
            return gson.fromJson(obj, typeOfT)
        }
        if (json?.isJsonArray == true) {
            val array = json.asJsonArray
            searchInArray(array)
            return gson.fromJson(array, typeOfT)
        }
        return gson.fromJson(json, typeOfT)
    }

    private fun searchInObject(obj: JsonObject) {
        val nullKey = ArrayList<String>()
        obj.keySet().forEach {
            val element = obj.get(it)
            when {
                element.isJsonNull -> nullKey.add(it)
                element.isJsonObject -> searchInObject(element.asJsonObject)
                element.isJsonArray -> searchInArray(element.asJsonArray)
            }
        }
        nullKey.forEach { obj.remove(it) }
    }

    private fun searchInArray(arr: JsonArray) {
        val nullIndex = LinkedList<Int>()
        arr.forEachIndexed { index, jsonElement ->
            when {
                jsonElement.isJsonNull -> nullIndex.addFirst(index)
                jsonElement.isJsonObject -> searchInObject(jsonElement.asJsonObject)
                jsonElement.isJsonArray -> searchInArray(jsonElement.asJsonArray)
            }
        }
        nullIndex.forEach { arr.remove(it) }
    }
}

创建TypeAdapterFactory

仅仅定义一个JsonDeserializer就只能对应一个数据类型,而我们定义的还是Any类型,这样就只能解析Any类型对应的数据,所以我们需要定义一个工厂,将所有类型的数据都指向我们的万能适配器。

private class GsonFactory(private val originGson: Gson) : TypeAdapterFactory {
    private val adapter = GsonAdapter(originGson)
    override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T>? {
        return TreeTypeAdapter.newFactoryWithMatchRawType(type, adapter).create(originGson, type)
    }
}

使用GsonFactory

在定义Gson的时候调用registerTypeAdapterFactory方法将工厂注册到Gson中,该工厂需要传入一个原始的解析器,用于移除空指针后的数据解析。

val gson = GsonBuilder().registerTypeAdapterFactory(GsonFactory(Gson())).create()

效果测试

定义两个数据模型,是其中一个模型作为另一个模型中某字段的类型,丰富测试数据的样式。

class Book {
    var name: String = "Book"
    var author: List<String> = listOf()
    var price: Double = 0.0
    var factory1: PrintFactory = PrintFactory()
    var factory2: PrintFactory = PrintFactory()
}

class PrintFactory {
    var name: String = "VIP"
    var location: String = "China"
    var enable: Boolean = true
}

编写测试代码

val content = """
    {"name":null,"author":["Mike","Apple","Jack",null,null],"price":56,"factory1":null,"factory2":{"name":"CCC","enable":false}}
    """.trimIndent()
val book: Book = AppGson.toObject(content)
Log.i("Test",AppGson.toJson(book))
//输出
//{"author":["Mike","Apple","Jack"],"factory1":{"enable":true,"location":"China","name":"VIP"},"factory2":{"enable":false,"location":"China","name":"CCC"},"name":"Book","price":56.0}

content模拟了从服务端接收到的数据,其中包含多个nullAppGson是我为方便Gson初始化和使用而封装的工具类。我先是使用AppGson将数据转换为Book对象,再通过AppGsonbook对象转换成json字符串,通过打印json字符串就可以验证null是否被正确处理。

通过下方的数据结果可以看出,原本null的字段都被替换成了数据模型中的默认值,完美的结果了空指针的问题,而且不用定义各种各样的适配器,同时也保留了数据模型中的默认值。

疑问

通过这一套方案的处理,成功的解决了null对于客户端的影响,避免了更多的后台同学遭遇不幸,但我还是不知道为什么要把null的值序列化出来,这样做有什么意义。希望了解的同学予以解答。

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

推荐阅读更多精彩内容