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

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

blog.csdn.net成就一亿技术人!

前言

上一篇博客我们聊了下gson在处理kotlin data class时的一些坑,感兴趣的可以了解一下:gson反序列化成data class时的坑

总结一下有一下两点

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

在文章末尾也介绍了解决办法就是不要使用gson,因为gson主要还是针对java的库,没有对kotlin做单独的支持。-
我们可以使用moshi或jackson来解决上面所说的问题。-
jackson的是spring boot 默认使用的son库,在服务端应用非常广泛,当然,Android中也可以用,而且也有给Retrofit提供专门的转换器:https://github.com/square/retrofit/tree/master/retrofit-converters/jackson-
,同时也针对kotlin提供了单独的支持库https://github.com/FasterXML/jackson-module-kotlin-
想要了解的可以去看下。

今天我们主要来看一下moshi的使用和实战。为什么选择moshi呢?

  • square出品-
    做Android的对square都不陌生,我们常用的 retrofit、okhttp、leakcanary等都是square在维护的。那moshi显然在搭配retrofit上有着得天独厚的优势,况且moshi还针对Android-
    在这里插入图片描述
  • 是一个相对比较新的开源库,站在巨人的肩膀上.-
    moshi的维护人员对gson也有过很多贡献,也借鉴了gson的一些思想,详情可以看 https://medium.com/square-corner-blog/moshi-another-json-processor-624f8741f703 了解一下

moshi的基础使用

官方文档 写的也比较详细了。-
我们先来使用一下moshi的基础功能,看一下有没有解决gson在反序列化data class遇到的问题。

文档介绍说是有两种使用方式,一种是使用kotlin反射,一种是代码生成器在编译期生成。

使用kotlin反射的方式会引入 kotlin-reflect,大约2.5M,对包体积敏感的需要注意下,但是,如果你有其他需求已经引入了kotlin-reflect,那就无需纠结这个问题了。

我们先用反射的方式使用吧,个人感觉相对通用一些,毕竟老项目很可能用到了反射相关的东西。

添加依赖

   implementation("com.squareup.moshi:moshi-kotlin:1.13.0")

基本使用

还是之前的例子,先搞个数据类

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

测试代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()

    /*创建对象*/
    val user = User("喻志强", 18)
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter(User::class.java)
    /*序列化*/
    val toJson = jsonAdapter.toJson(user)
    println("toJson = ${toJson}")
    /*反序列化*/
    val fromJson = jsonAdapter.fromJson(toJson)
    println("fromJson = ${fromJson}")

运行结果-
在这里插入图片描述-
可以看到,基础使用还是很简单的,主要就是创建moshi,生成一下JsonAdapter后就可以愉快的序列化和反序列化了。

需要注意的是,如果使用kotlin反射的方式,需要加上

在这里插入图片描述

不然会报下面错误-
Cannot serialize Kotlin type json.data.User. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapterFactory from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.

然后我们来看看之前在使用gson反序列化data class时遇到的问题moshi有没有解决掉

属性声明时值不能为null,结果反序列化后值为null,跟预期不符

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter(User::class.java)
    val jsonStr = """
        {"age":18}
    """.trimIndent()
    val user = jsonAdapter.fromJson(jsonStr)
    println("user = ${user}")

运行-
在这里插入图片描述-
可以看到,如果是gson的话,这样写是不会有报错的。-
moshi的话可以正确的拦截掉这种异常情况,提示我们name字段缺失了。

再看下json中name值为null的情况

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter(User::class.java)
    val jsonStr = """
        {"name":null,"age":18}
    """.trimIndent()
    val user = jsonAdapter.fromJson(jsonStr)
    println("user = ${user}")

运行-
在这里插入图片描述-
可以看到,很明确的告诉了我们name值不能为空,从根源上拦截掉了属性声明时值不能为null,结果反序列化后值为null的问题

默认值可能不生效,可能被null覆盖

再来看下默认值失效的问题,还是之前的例子

数据类

data class User(
    val name: String="xeon",
    var age: Int,
)

代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter(User::class.java)
    val jsonStr = """
        {"age":18}
    """.trimIndent()
    val user = jsonAdapter.fromJson(jsonStr)
    println("user = ${user}")

运行结果:-
在这里插入图片描述

可以看到,默认值是正常生效的,符合预期。

再试试值传null的情况

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter(User::class.java)
    val jsonStr = """
        {"name":null,"age":18}
    """.trimIndent()
    val user = jsonAdapter.fromJson(jsonStr)
    println("user = ${user}")

运行结果-
在这里插入图片描述-
可以看到,也是ok的,提前把异常情况给捕获到了。

从基本的使用我们可以看到moshi确实解决了gson的那些坑。

下面我们看下开发时比较常用的使用场景,用moshi怎么写。


moshi处理list的场景

直接将json转成list是很常见的场景,官方文档也已经告诉我们怎么用了,就不多说了,直接上代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()

    val users = mutableListOf<User>()
    (1..3).forEach {
        val user = User("喻志强", 25 + it)
        users.add(user)
    }
    /*声明adapter,指定要处理的类型*/
    val parameterizedType = Types.newParameterizedType(List::class.java, User::class.java)
    val jsonAdapter = moshi.adapter<List<User>>(parameterizedType)
    val toJson = jsonAdapter.toJson(users)
    println("toJson = ${toJson}")
    val jsonStr = """
        [{"name":"喻志强","age":26},{"name":"喻志强","age":27},{"name":"喻志强","age":28}]
    """.trimIndent()
    val fromJson = jsonAdapter.fromJson(jsonStr)
    println("fromJson = ${fromJson}")
    if (fromJson != null) {
        fromJson.forEach {
            println("it.age = ${it.age}")
        }
    }

运行:-
在这里插入图片描述

可以看到,通过配置一个ParameterizedType生成一个adapter即可。adapter会根据你指定的类型来处理。


moshi 处理泛型的场景

我们在日常开发对接口时,都会定一个基类,如下

data class BaseResp<T>(
    val code: Int,
    val msg: String,
    val data: T,
)

这种是非常常见的情况,类型是BaseResp,里面的data是一个泛型,这种要怎么写呢?-
来看一下

测试代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    val user = User("喻志强", 28)
    val baseResp = BaseResp<User>(200, "成功", user)
    /*声明adapter,指定要处理的类型*/
    val jsonAdapter = moshi.adapter<BaseResp<User>>(BaseResp::class.java)
    val toJson = jsonAdapter.toJson(baseResp)
    println("toJson = ${toJson}")

运行-
在这里插入图片描述

直接传BaseResp::class.java 生成的jsonAdapter 显然是不认识User这个类型的,那参考List的使用场景,我们可以这样写

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()

    val baseResp = BaseResp<User>(200, "请求成功", User("xeon", 28))
    /*声明adapter,指定要处理的类型*/
    val parameterizedType = Types.newParameterizedType(BaseResp::class.java, User::class.java)
    val jsonAdapter = moshi.adapter<BaseResp<User>>(parameterizedType)
    val toJson = jsonAdapter.toJson(baseResp)
    println("toJson = ${toJson}")
    val jsonStr = """
        {"code":200,"msg":"请求成功","data":{"name":"xeon","age":28}}
    """.trimIndent()
    val fromJson = jsonAdapter.fromJson(jsonStr)
    println("fromJson = ${fromJson}")

运行一下看看-
在这里插入图片描述-
嗯,看运行结果没啥问题,那解析泛型是ok了。

等等,这个是比较简单的场景,我们把数据整的稍微复杂点试试,比如,数据里面有个List

加一个数据类

data class Hobby(
    val type: String,
    var name: String,
)

user增加个list类型的字段

data class User(
    val name: String = "xeon",
    var age: Int,
    var hobby: List<Hobby>,
)

测试代码:

    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    val baseResp = BaseResp<User>(200, "请求成功", User("xeon", 28, arrayListOf(Hobby("游戏", "王者荣耀"), Hobby("运行", "跑步"))))
    /*声明adapter,指定要处理的类型*/
    val parameterizedType = Types.newParameterizedType(BaseResp::class.java, User::class.java)
    val jsonAdapter = moshi.adapter<BaseResp<User>>(parameterizedType)
    val toJson = jsonAdapter.toJson(baseResp)
    println("toJson = ${toJson}")
    val jsonStr = """
        {"code":200,"msg":"请求成功","data":{"name":"xeon","age":28,"hobby":[{"type":"游戏","name":"王者荣耀"},{"type":"运行","name":"跑步"}]}}
    """.trimIndent()
    val fromJson = jsonAdapter.fromJson(jsonStr)
    println("fromJson = ${fromJson}")

运行一下:-
在这里插入图片描述

看起来也是ok的

moshi处理泛型直接是个List的场景-
我们再来看看data直接就是一个List的场景

数据类跟上面一样不变

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    val users = mutableListOf<User>()
    (1..3).forEach {
        val user = User("xeon", 28, arrayListOf(Hobby("游戏", "王者荣耀"), Hobby("运行", "跑步")))
        users.add(user)
    }

    val baseResp = BaseResp<List<User>>(200, "请求成功", users)

    /*声明adapter,指定要处理的类型*/
    val parameterizedType = Types.newParameterizedType(BaseResp::class.java, List::class.java)
    val jsonAdapter = moshi.adapter<BaseResp<List<User>>>(parameterizedType)
    val toJson = jsonAdapter.toJson(baseResp)
    println("toJson = ${toJson}")
    val jsonStr = """
       {"code":200,"msg":"请求成功","data":[{"name":"xeon","age":28,"hobby":[{"type":"游戏","name":"王者荣耀"},{"type":"运行","name":"跑步"}]},{"name":"xeon","age":28,"hobby":[{"type":"游戏","name":"王者荣耀"},{"type":"运行","name":"跑步"}]},{"name":"xeon","age":28,"hobby":[{"type":"游戏","name":"王者荣耀"},{"type":"运行","name":"跑步"}]}]}
    """.trimIndent()
    val fromJson = jsonAdapter.fromJson(jsonStr)
    println("fromJson = ${fromJson}")

运行结果如下-
在这里插入图片描述

嗯,看起来也很正常。一切都是那么的美好…

到这里的时候,心里突然一紧,因为之前在使用gson转list的时候遇到过泛型擦除的坑,详情可以看这篇文章-
Gson直接将json转list示例 (TypeToken)以及通过内联函数结合reified简化代码

那moshi是不是也会有这个问题呢,回头再仔细看下运行结果对比对比一下

之前的运行结果,可以看到输出是有具体类型的-
在这里插入图片描述-
刚才的运行结果,没有类型-
在这里插入图片描述

这时心里一凉,赶紧遍历输出下属性值试一下,加上一下代码

    if (fromJson!=null){
        fromJson.data.forEach {
            println("it.name = ${it.name}")
        }

运行-
在这里插入图片描述-
卧槽,不出所料,果然报错了-
com.squareup.moshi.LinkedHashTreeMap cannot be cast to xxx-
这不就跟之前使用gson时遇到的坑是一样的吗…

那问题出在哪呢,大概率还是类型那里有问题,

打个断点跑一下看看-
在这里插入图片描述-
果然类型不全,因为我们只给了List,并没有给User类型,所以adapter不认识也很正常

那我们要怎么告诉adapter识别到User类型呢?看一眼源码,发现typeArguments是可变参数,也就是可以传多个

在这里插入图片描述

那我们直接把User加进去试一下

    val parameterizedType =
        Types.newParameterizedType(BaseResp::class.java, List::class.java,User::class.java)

然后运行发现还是报错,断点看一下类型如下-
在这里插入图片描述-
上面那种应该是适用于map的key、value的形式,我们期望的type应该是下面这样子的,

json.data.BaseResp<java.util.List<json.data.User>>

嗯,那就简单了呀,这不就是套娃吗?代码如下

    /*重点在这 先生成List<User>这种类型*/
    val listUserType = Types.newParameterizedType(List::class.java, User::class.java)
    /* 最后是BaseResp<List<User>> */
    val parameterizedType =
        Types.newParameterizedType(BaseResp::class.java, listUserType)

断点看下类型-
在这里插入图片描述-
嗯,这下就对了。-
再看下运行结果-
在这里插入图片描述

OK了,没毛病了。

至此,相对复杂的数据我们也没啥问题了。


moshi 字段映射,序列化反序列化字段忽略,格式化等

字段映射和字段忽略-
通过 @Json(name = "map_filed", ignore = false) 注解配置即可,例如

data class User(
    @Json(name = "name_filed")
    val name: String = "xeon",
    var age: Int,
    @Json(ignore = true)
    var hobby: List<Hobby> = arrayListOf(),
)

比较简单,直接上代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    val user = User("yzq", 28, arrayListOf(Hobby("游戏", "王者")))
    val jsonAdapter = moshi.adapter<User>(User::class.java)
    val toJson = jsonAdapter.toJson(user)
    println("toJson = ${toJson}")
    val jsonStr = """
        {"name_filed":"xeon","age":28,"hobby":[{"type":"游戏","name":"王者"}]}
    """.trimIndent()

    val fromJson = jsonAdapter.fromJson(jsonStr)
    println("fromJson = ${fromJson}")

运行结果-
在这里插入图片描述

moshi tojson格式化-
有时候我们想toJson的时候是格式化后的,好看一些,可以用 indent

示例代码

    /*创建moshi*/
    val moshi = Moshi.Builder()
        .addLast(KotlinJsonAdapterFactory())//使用kotlin反射处理,要加上这个
        .build()
    val user = User("yzq", 28, arrayListOf(Hobby("游戏", "王者")))
    val jsonAdapter = moshi.adapter<User>(User::class.java)
    val toJson = jsonAdapter.indent(" ").toJson(user)
    println(toJson)

运行结果-
在这里插入图片描述-
还可以自定义字符例如indent("》》》")-
在这里插入图片描述


moshi 封装

跟使用gson一样,我们也可以封装一个工具类,方便使用,这里放一个我自己用的简单封装,仅供参考

import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import java.lang.reflect.ParameterizedType

/**
 * @description: 基于moshi的json转换封装
 * @author : yuzhiqiang (zhiqiang.yu.xeon@gmail.com)
 * @date   : 2022/3/13
 * @time   : 6:29 下午
 */

object MoshiUtil {

    val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()

    fun <T> toJson(adapter: JsonAdapter<T>, src: T, indent: String = ""): String {
        try {
            return adapter.indent(indent).toJson(src)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return ""

    }

    /**
     * T 类型对象序列化为 json
     * @param src T
     * @param indent String
     * @return String
     */
    inline fun <reified T> toJson(src: T, indent: String = ""): String {
        val adapter = moshi.adapter(T::class.java)
        return this.toJson(adapter = adapter, src = src, indent = indent)
    }


    /**
     * 将 T 序列化为 json,指定 parameterizedType,适合复杂类型
     * @param src T
     * @param parameterizedType ParameterizedType
     * @param indent String
     * @return String
     */
    inline fun <reified T> toJson(src: T, parameterizedType: ParameterizedType, indent: String = ""): String {
        val adapter = moshi.adapter<T>(parameterizedType)
        return this.toJson(adapter = adapter, src = src, indent = indent)
    }

    inline fun <reified T> fromJson(adapter: JsonAdapter<T>, jsonStr: String): T? {
        try {
            return adapter.fromJson(jsonStr)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }

    /**
     * json 反序列化为 T
     * @param jsonStr String
     * @return T?
     */
    inline fun <reified T> fromJson(jsonStr: String): T? {
        val adapter = moshi.adapter(T::class.java)
        return this.fromJson(adapter, jsonStr)
    }

    /**
     * json 反序列化为 MutableList<T>
     * @param jsonStr String
     * @return MutableList<T>?
     */
    inline fun <reified T> fromJsonToList(jsonStr: String): MutableList<T>? {
        val parameterizedType = Types.newParameterizedType(MutableList::class.java, T::class.java)
        return fromJson<MutableList<T>>(jsonStr, parameterizedType)
    }

    /**
     * json 反序列化为 T, 指定 parameterizedType,复杂数据用
     * @param jsonStr String
     * @param parameterizedType ParameterizedType
     * @return T?
     */
    inline fun <reified T> fromJson(jsonStr: String, parameterizedType: ParameterizedType): T? {
        val adapter = moshi.adapter<T>(parameterizedType)
        return this.fromJson(adapter = adapter, jsonStr = jsonStr)
    }

}

针对BaseResp的处理,我们可以写一个扩展方法,如下:

/**
 * baseResp 转 json
 * @receiver BaseResp<T>
 * @return String
 */
inline fun <reified T> BaseResp<T>.toJson(): String {
    val parameterizedType = Types.newParameterizedType(BaseResp::class.java, T::class.java)
    return MoshiUtil.toJson(this, parameterizedType)
}


/**
 * json 转 BaseResp<T>
 * @receiver String
 * @return BaseResp<T>?
 */
inline fun <reified T> String.toBaseResp(): BaseResp<T>? {
    val newParameterizedType = Types.newParameterizedType(BaseResp::class.java, T::class.java)
    return MoshiUtil.fromJson<BaseResp<T>>(this, newParameterizedType)
}

/**
 * json 转 BaseResp<MutableList<T>>
 * @receiver String
 * @return BaseResp<MutableList<T>>?
 */
inline fun <reified T> String.toBaseRespList(): BaseResp<MutableList<T>>? {
    val insideType = Types.newParameterizedType(List::class.java, T::class.java)
    val parameterizedType = Types.newParameterizedType(BaseResp::class.java, insideType)
    return MoshiUtil.fromJson<BaseResp<MutableList<T>>>(this, parameterizedType)
}

使用示例

    val userList = mutableListOf<User>()
    (1..10).forEach {
        val user = User("name${it}", it, arrayListOf(Hobby("类型", "爱好${it}")))
        userList.add(user)
    }

    /*对象类型*/
    val userJsonStr = MoshiUtil.toJson(userList.get(0))
    println("userList = ${userList}")
    val user = MoshiUtil.fromJson<User>(userJsonStr)
    println("user = ${user}")

    /*List*/
    val userListJsonStr = MoshiUtil.toJson(userList)
    println("userListJsonStr = ${userListJsonStr}")
    val userMutableList = MoshiUtil.fromJsonToList<User>(userListJsonStr)
    println("userMutableList = ${userMutableList}")

    /*泛型*/
    val baseResp = BaseResp(200, "ok", userList)
    val baseRespJsonStr = baseResp.toJson()
    println("baseRespJsonStr = ${baseRespJsonStr}")
    val baseRespList = baseRespJsonStr.toBaseRespList<User>()
    println("baseRespList = ${baseRespList}")

运行结果

在这里插入图片描述


moshi配合retrofit

跟gson类似,比较简单

添加依赖

api "com.squareup.retrofit2:converter-moshi:$retrofit"

创建Retrofit的时候把moshi转换器添加进去

 val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
 
 retrofit = Retrofit.Builder()
            .baseUrl(ServerConstants.apiUrl)
            .client(initOkhttpClient())
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()

剩下的就是正常使用Retrofit即可。

好了,到这里我们日常使用moshi基本没啥大问题了,如果你需要自定义Adapter或者想用代码生成的方式使用,直接按照官方文档做就好了,比较简单,篇幅有点长,就不展开了。


2022-06-11更新

在封装完之后总觉得不够简洁,使用起来也比较麻烦,于是对封装进行了重构优化,及其简洁,感兴趣可以看 moshi 极简封装 这篇文章。


posted @ 2023-04-21 10:55  cps666  阅读(202)  评论(0编辑  收藏  举报