对kotlin友好的现代 JSON 库 moshi 基本使用和实战
Posted XeonYu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了对kotlin友好的现代 JSON 库 moshi 基本使用和实战相关的知识,希望对你有一定的参考价值。
前言
上一篇博客我们聊了下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以上是关于对kotlin友好的现代 JSON 库 moshi 基本使用和实战的主要内容,如果未能解决你的问题,请参考以下文章
对kotlin友好的现代 JSON 库 moshi 基本使用和实战