高效的 Json 解析框架 kotlinx.serialization
一、引出问题
你是否有在使用 Gson 序列化对象时,见到如下异常:
Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type.
什么时候会出现如此异常。下面举个栗子:
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
sealed class Gender
object Male: Gender()
object Female: Gender()
data class Student(
val id: Int,
val name: String,
val gender: Gender
)
fun main() {
val list1 = listOf(
Student(1001, "Jimy", Male),
Student(1002, "Lucy", Female),
Student(1003, "HanMeimei", Female),
Student(1004, "LiLei", Male)
)
println("list1: $list1")
val jsonString = Gson().toJson(list1)
println("jsonString: $jsonString")
try {
val typeToken = object : TypeToken<List<Student>>() {}.type
val list2: List<Student> = Gson().fromJson(jsonString, typeToken)
println("list2: $list2")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
上面的代码,执行结果如下:
list1: [Student(id=1001, name=Jimy, gender=serialize.gson.Male@79fc0f2f), Student(id=1002, name=Lucy, gender=serialize.gson.Female@50040f0c), Student(id=1003, name=HanMeimei, gender=serialize.gson.Female@50040f0c), Student(id=1004, name=LiLei, gender=serialize.gson.Male@79fc0f2f)]
jsonString: [{"id":1001,"name":"Jimy","gender":{}},{"id":1002,"name":"Lucy","gender":{}},{"id":1003,"name":"HanMeimei","gender":{}},{"id":1004,"name":"LiLei","gender":{}}]
catch: Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type. Class name: serialize.gson.Gender
从这个输出结果,我们可以看到两个问题:
list1经过序列化,得到的jsonString中,gender属性是空。jsonString反序列化过程中发生了异常。
二、解决问题
异常信息已经指明了问题的解决方案
Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type.
抽象类无法实例化!为此类型注册 InstanceCreator 或 TypeAdapter。
其实也很好理解。 sealed class、abstract class、interface 都是抽象的,不能直接被实例化。对于抽象类的子类或者接口的实现类,应该明确制定序列化和反序列化的规则。由于我们没有注册 TypeAdapter, 默认的 TypeAdapter ,将 Gender 属性序列化为了空对象。在进行反序列化时,空对象不知道应该如何反序列化,所以抛出了如下的异常。
解决办法之一,在序列化和反序列化时,需要使用 Gson 的 registerTypeAdapter 或 registerTypeHierarchyAdapter 方法来处理密封类的子类。
首先为抽象类/接口创建一个 TypeAdapter
class GenderTypeAdapter: TypeAdapter<Gender>() {
override fun write(out: JsonWriter?, value: Gender?) {
out?.value(value?.javaClass?.name)
}
override fun read(`in`: JsonReader?): Gender {
return when(val className = `in`?.nextString()) {
Male::class.java.name -> Male
Female::class.java.name -> Female
else -> throw IllegalArgumentException("Unknown class name: $className")
}
}
}
然后为 Gson 对象注册该 typeAdapter
fun main() {
val list1 = listOf(
Student(1001, "Jimy", Male),
Student(1002, "Lucy", Female),
Student(1003, "HanMeimei", Female),
Student(1004, "LiLei", Male)
)
println("list1: $list1")
// I'm here
val jsonString = GsonBuilder().registerTypeAdapter(Gender::class.java, GenderTypeAdapter()).create().toJson(list1)
println("jsonString: $jsonString")
try {
val typeToken = object : TypeToken<List<Student>>() {}.type
// I'm here
val list2: List<Student> = GsonBuilder().registerTypeAdapter(Gender::class.java, GenderTypeAdapter()).create().fromJson(jsonString, typeToken)
println("list2: $list2")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
此时执行结果如下:
list1: [Student(id=1001, name=Jimy, gender=serialize.gson.Male@79fc0f2f), Student(id=1002, name=Lucy, gender=serialize.gson.Female@50040f0c), Student(id=1003, name=HanMeimei, gender=serialize.gson.Female@50040f0c), Student(id=1004, name=LiLei, gender=serialize.gson.Male@79fc0f2f)]
jsonString: [{"id":1001,"name":"Jimy","gender":"serialize.gson.Male"},{"id":1002,"name":"Lucy","gender":"serialize.gson.Female"},{"id":1003,"name":"HanMeimei","gender":"serialize.gson.Female"},{"id":1004,"name":"LiLei","gender":"serialize.gson.Male"}]
list2: [Student(id=1001, name=Jimy, gender=serialize.gson.Male@79fc0f2f), Student(id=1002, name=Lucy, gender=serialize.gson.Female@50040f0c), Student(id=1003, name=HanMeimei, gender=serialize.gson.Female@50040f0c), Student(id=1004, name=LiLei, gender=serialize.gson.Male@79fc0f2f)]
Ok, 没有问题。
那... registerTypeAdapter 和 registerTypeHierarchyAdapter 两个方法有什么区别呢?
它们的主要区别在于注册对象的范围不同。
registerTypeAdapter 用于为特定的 Java 对象或类型注册自定义的序列化和反序列化逻辑。使用 TypeAdapter,可以在 Gson 序列化或反序列化特定对象或类型时,对其进行自定义处理。TypeAdapter 只会被应用于所注册的对象或类型。
registerTypeHierarchyAdapter 方法则是用于为特定类及其子类注册自定义的序列化和反序列化逻辑。使用 registerTypeHierarchyAdapter 方法,可以为一个类及其子类注册自定义的序列化和反序列化逻辑,这个逻辑将被应用于该类及其所有子类。这在处理一组类继承结构时非常有用。
在使用 registerTypeHierarchyAdapter 方法时,需要注意一点,即 Gson 会遍历所有的子类来找到最合适的 TypeAdapter,因此要确保该 TypeAdapter 能够正确处理所有的子类。如果某个子类没有对应的处理逻辑,或者处理逻辑有误,就可能导致序列化或反序列化失败。
因此,如果要为一组类继承结构注册自定义的序列化和反序列化逻辑,可以使用 registerTypeHierarchyAdapter 方法;如果只需要为某个具体的 Java 对象或类型注册自定义的序列化和反序列化逻辑,则可以使用 TypeAdapter。
三、用 kotlinx.serialization 进行Kotlin JSON序列化
Gson 是针对 java 对象的序列化框架。基于 Kotlin 对象使用 Gson 框架,会失去 Kotlin 的一些重要特性,比如:
- 非空类型安全。比如 Kotlin 类的属性定义为非空类型时,仍然可以将一个 null 赋值给它创建一个对象。
- 参数默认值没有效果。Kotlin 属性可以赋予默认值。但是当使用 Gson 时,将会失去效果。
修改之前的例子:
sealed class Gender
object Male: Gender()
object Female: Gender()
data class Student(
val id: Int,
val name: String = "unknown",
val gender: Gender
)
fun main() {
val json = """
{
"id": 1005
}
""".trimIndent()
try {
val stu = Gson().fromJson(json, Student::class.java)
println("stu: $stu")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
这里我们在定义 Student 类是,给 name 属性指定了一个默认值 unknown, 在进行反序列化时,没有指定 name 和 gender, 看看执行结果:
stu: Student(id=1005, name=null, gender=null)
结果也表明,name 的默认值没有成功,并且 name 和 gender 都赋值为 null 了。
针对上述问题有很多解决办法。但是这里,我要介绍一个新的 Json 框架,Kotlin 团队开发的一个 native 支持的库 kotlinx.serialization, 这个库支持JVM,JavaScript,Native所有平台,同时也支持多种格式的序列化——JSON,CBOR,protocol buffers等等。
3.1 kotlinx.serialization 的使用
- plugins 引入:
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version("1.4.30")
}
- dependencies 引入:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
- 通过添加
@Serializable注解,给类进行序列化
package serialize.ktxSerialization
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
sealed class Gender
@Serializable
object Male: Gender()
@Serializable
object Female: Gender()
@Serializable
data class Student(
val id: Int,
val name: String = "unknown",
val gender: Gender
)
注意:所涉及到的抽象类极其子类都需要加上该注解。
测试代码:
fun main() {
val json = """
{
"id": 1005
}
""".trimIndent()
try {
val stu = Json.decodeFromString<Student>(json)
println("stu: $stu")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
反序列化的关键方法:
Json.decodeFromString()
执行报错了:
catch: Field 'gender' is required for type with serial name 'serialize.ktxSerialization.Student', but it was missing at path: $
错误信息指出: gender 属性是必须的。那我们应该如何该如何添加 gender 属性呢?
不急,我们先序列化看看生成的是什么。
fun main() {
val student = Student(1006, "James", Male)
val jsonString = Json.encodeToString(student)
println("jsonString: $jsonString")
}
执行结果如下:
jsonString: {"id":1006,"name":"James","gender":{"type":"serialize.ktxSerialization.Male"}}
我们看到,Student 对象序列化之后, gender 对应的 value 是
{"type":"serialize.ktxSerialization.Male"}
这里是完整的包名类名。
到这里,我们再手动构造验证一下:
fun main() {
val json = """
{
"id": 1005,
"gender": {"type": "serialize.ktxSerialization.Female"}
}
""".trimIndent()
try {
val stu = Json.decodeFromString<Student>(json)
println("stu: $stu")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
执行结果:
stu: Student(id=1005, name=unknown, gender=serialize.ktxSerialization.Female@36d64342)
可以看到,反序列化成功,生成的对象,name 属性赋了默认值。
另外需要注意的是:如果在定义 Kotlin 的类中某个属性,没有指定默认值,即便该属性是可空类型,反序列化时也一定要赋值才能执行成功。
修改下例子:
package serialize.ktxSerialization
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
sealed class Gender
@Serializable
object Male: Gender()
@Serializable
object Female: Gender()
@Serializable
data class Student(
val id: Int,
val name: String?, // 注意这里
val gender: Gender
)
fun main() {
val json = """
{
"id": 1005,
"gender": {"type": "serialize.ktxSerialization.Female"}
}
""".trimIndent()
try {
val stu = Json.decodeFromString<Student>(json)
println("stu: $stu")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
我把 name 设置为可空类型,但是没有默认值。这时反序列化是会失败的:
catch: Field 'name' is required for type with serial name 'serialize.ktxSerialization.Student', but it was missing at path: $
给 name 属性赋值为 null, 则执行成功
fun main() {
val json = """
{
"id": 1005,
"name", null,
"gender": {"type": "serialize.ktxSerialization.Female"}
}
""".trimIndent()
try {
val stu = Json.decodeFromString<Student>(json)
println("stu: $stu")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
结果:
stu: Student(id=1005, name=null, gender=serialize.ktxSerialization.Female@340f438e)
3.2 用 kotlinx.serialization 解决本文开头的问题
对于本文开头引出的问题,如果使用 kotlinx.serialization,则该问题即可轻松解决。
直接上代码:
fun main() {
val list1 = listOf(
Student(1001, "Jimy", Male),
Student(1002, "Lucy", Female),
Student(1003, "HanMeimei", Female),
Student(1004, "LiLei", Male)
)
println("list1: $list1")
val jsonString = Json.encodeToString(list1)
println("jsonString: $jsonString")
try {
val list2 = Json.decodeFromString<List<Student>>(jsonString)
println("list2: $list2")
} catch (ex: Exception) {
println("catch: ${ex.message}")
}
}
执行结果:
list1: [Student(id=1001, name=Jimy, gender=serialize.ktxSerialization.Male@531d72ca), Student(id=1002, name=Lucy, gender=serialize.ktxSerialization.Female@22d8cfe0), Student(id=1003, name=HanMeimei, gender=serialize.ktxSerialization.Female@22d8cfe0), Student(id=1004, name=LiLei, gender=serialize.ktxSerialization.Male@531d72ca)]
jsonString: [{"id":1001,"name":"Jimy","gender":{"type":"serialize.ktxSerialization.Male"}},{"id":1002,"name":"Lucy","gender":{"type":"serialize.ktxSerialization.Female"}},{"id":1003,"name":"HanMeimei","gender":{"type":"serialize.ktxSerialization.Female"}},{"id":1004,"name":"LiLei","gender":{"type":"serialize.ktxSerialization.Male"}}]
list2: [Student(id=1001, name=Jimy, gender=serialize.ktxSerialization.Male@531d72ca), Student(id=1002, name=Lucy, gender=serialize.ktxSerialization.Female@22d8cfe0), Student(id=1003, name=HanMeimei, gender=serialize.ktxSerialization.Female@22d8cfe0), Student(id=1004, name=LiLei, gender=serialize.ktxSerialization.Male@531d72ca)]
这里很好理解:
在没有给 Gson 注册 TypeAdapter 的时候,使用默认的 TypeAdapter, 把引用类型序列化为了空。反序列化时才会失败。而是用 kotlinx.serialization ,相当于默认提供了一个序列化和反序列方案。所以直接可以成功。无需我们自己定义序列化和反序列化的规则。
四、总结
最后对本文做个总结:
- 在使用 Gson 进行序列化和反序列过程中。要注意多态的情况下。需要自己注册 TypeAdapter。
- 如果使用 Kotlin 开发,优先使用高效的序列化框架:kotlinx.serialization。
kotlinx.serialization 具有如下特性:
- 类型安全:满足 Kotlin 的强制类型安全。可处理 Kotlin 的可空类型。
- 支持属性默认值:解析 JSON 的时候支持 Kotlin 类中属性的默认值。
- 支持泛型类型:API在序列化和反序列化泛型类型的时候非常简单也非常高效。
- 序列化字段名:当 json 的 key 和字段名不一致时,可以通过
@SerialName给字段进行序列化。 同 Gson 中的@SerializedName。 - 序列化引用对象:当属性的类型是引用类型时,对该类型也需要使用
@Serializable注解。 - 数据校验:可以再 json 反序列化时对数据进行校验。
- 支持 Retrofit 库。详见针对Retrofit 2 Converter.Factory的Kotlin序列化的库。
kotlinx.serialization 有很多优秀的特性。本文算是抛砖引玉。更多特性,请自己手动 Coding 体验。
最后附上 kotlinx.serialization 的官方文档:https://github.com/Kotlin/kotlinx.serialization
高效的 Json 解析框架 kotlinx.serialization的更多相关文章
- Spring Boot默认的JSON解析框架设置
方案一:启动类继承WebMvcConfigurerAdapter,覆盖方法configureMessageConverters ... @SpringBootApplication public cl ...
- 十七、springboot配置FastJson为Spring Boot默认JSON解析框架
前提 springboot默认自带json解析框架,默认使用jackson,如果使用fastjson,可以按照下列方式配置使用 1.引入fastjson依赖库: maven: <dependen ...
- springboot使用fastJson作为json解析框架
springboot使用fastJson作为json解析框架 springboot默认自带json解析框架,默认使用jackson,如果使用fastjson,可以按照下列方式配置使用 〇.搭建spri ...
- 移动架构-json解析框架
JSON在现在数据传输中占据着重要地位,相比于xml,其解析和构成都要简单很多,第三方的解析框架也不胜枚举,这里之所以要自定义一个json解析框架,一方面是更好的了解json解析过程,另一方面是有时候 ...
- 4. 使用别的json解析框架【从零开始学Spring Boot】
转载:http://blog.csdn.net/linxingliang/article/details/51585921 此文章已经废弃,请看新版的博客的完美解决方案: 78. Spring Boo ...
- (4)Spring Boot使用别的json解析框架【从零开始学Spring Boot】
此文章已经废弃,请看新版的博客的完美解决方案: 78. Spring Boot完美使用FastJson解析JSON数据[从零开始学Spring Boot] http://412887952-qq-co ...
- 将SpringBoot默认Json解析框架jackson替换成fastjson
步骤一:引入依赖<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson< ...
- Android JSON解析库Gson和Fast-json的使用对比和图书列表小案例
Android JSON解析库Gson和Fast-json的使用对比和图书列表小案例 继上篇json解析,我用了原生的json解析,但是在有些情况下我们不得不承认,一些优秀的json解析框架确实十分的 ...
- Android总结之json解析(FastJson Gson 对比)
前言: 最近为了统一项目中使用的框架,发现项目中用到了两种json解析框架,他们就是当今非常主流的json解析框架:google的Gson 和阿里巴巴的FastJson,为了废除其中一个所以来个性能和 ...
- 年年出妖事,一例由JSON解析导致的"薛定谔BUG"排查过程记录
前言 做开发这么多年,也碰到无数的bug了.不过再复杂的bug,只要仔细去研读代码,加上debug,总能找到原因. 但是最近公司内碰到的这一个bug,这个bug初看很简单,但是非常妖孽,在一段时间内我 ...
随机推荐
- 系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
虚拟内存 虚拟内存是一种操作系统提供的机制,用于将每个进程分配的独立的虚拟地址空间映射到实际的物理内存地址空间上.通过使用虚拟内存,操作系统可以有效地解决多个应用程序直接操作物理内存可能引发的冲突问题 ...
- 2.4 PE结构:节表详细解析
节表(Section Table)是Windows PE/COFF格式的可执行文件中一个非常重要的数据结构,它记录了各个代码段.数据段.资源段.重定向表等在文件中的位置和大小信息,是操作系统加载文件时 ...
- 论文精读:带有源标签自适应的半监督域适应(Semi-Supervised Domain Adaptation with Source Label Adaptation)
Semi-Supervised Domain Adaptation with Source Label Adaptation 具有源标签适应的半监督域适应 原文链接 Abstract 文章指出当前的半 ...
- nmcli 命令设置网络
nmcli 命令设置网络 设置静态 IP 地址 sudo nmcli connection modify "连接名称" ipv4.addresses IP地址/子网掩码 设置网关 ...
- [Mysql] 存储过程简单理解
什么是存储过程 简单的说, 就是一组SQL语句集, 功能强大, 可以实现一些比较复杂的逻辑功能. 其实就和编程语言的面向过程函数一样. ps: 存储过程与触发器类似, 但存储过程是主动调用, 触发器是 ...
- mpi转以太网连接200plc以太网监控同时与步科触摸屏通信
西门子PLC200 226PLC转以太网通过PPI-ETH-XD1.0集中采集不占用编程口同时与步科触摸屏通信 现有设备及联网要求客户车间内有6台纺机设备,控制系统采用西门子PLC,型号为CPU226 ...
- Python基础知识——函数的基本使用、函数的参数、名称空间与作用域、函数对象与闭包、 装饰器、迭代器、生成器与yield、函数递归、面向过程与函数式(map、reduce、filter)
文章目录 1 函数的基本使用 一 引入 二 定义函数 三 调用函数与函数返回值 2 函数的参数 一 形参与实参介绍 二 形参与实参的具体使用 2.1 位置参数 2.2 关键字参数 2.3 默认参数 2 ...
- .Net析构函数再论(CLR源码级的剖析)
前言 碰到一些问题,发觉依旧没有全面了解完全析构函数.本篇继续看下析构函数的一些引申知识. 概述 析构函数目前发现的总共有三个标记,这里分别一一介绍下.先上一段代码: internal class P ...
- .Net核心级的性能优化(GC篇)
1.前言 大部分人对于.Net性能优化,都停留在业务层面.或者简单的.Net框架配置层面.本篇来看下.Net核心部分GC垃圾回收配置:保留VM,大对象,独立GC,节省内存等.Net8里面有很多的各种G ...
- CF1338A
题目简化和分析: \(a_{i}\ge a_{i-1}\) 已经满足直接跳过 \(a_{i}<a_{i-1}\) 我们就要将其的差进行二进制的分解,使得 \(a_{i-1}=a_i\) 我也不知 ...