一 JSON 基本概念
JSON(JavaScript Object Notation, JS 对象简谱),是一种轻量级的数据交换格式。
在 JSON 出现之前,大家一直用 XML 来传递数据。因为 XML 是一种纯文本格式,所以它适合在网络上交换数据。XML 本身不算复杂,但是,加上 DTD、XSD、XPath、XSLT 等一大堆复杂的规范以后让它变得复杂、难以掌握。
终于在 2002 年,道格拉斯·克罗克福特(Douglas Crockford)发明了 JSON 这种超轻量级的数据交换格式。它基于 ECMAScript (European Computer Manufacturers Association, 欧洲计算机协会制定的 js 规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
在 JSON 中,一共只有以下几种数据类型:
- number:和 JavaScript 的
number
完全一致; - boolean:同 JavaScript 的
true
或false
; - string:同 JavaScript 的
string
; - null:同 JavaScript 的
null
; - array:同 JavaScript 的
Array
表示方式——[]
; - object:同 JavaScript 的
{ ... }
表示方式。
以及上面的任意组合。
并且,JSON 还规定字符集必须是 UTF-8,表示多语言就没有问题了。为了统一解析,JSON 的字符串规定必须用双引号 ""
,Object 的键也必须用双引号""
。
把任何 JavaScript 对象变成 JSON,就是把这个对象序列化成一个 JSON 格式的字符串,这样才能够通过网络传递给其他计算机。
如果我们收到一个 JSON 格式的字符串,只需要把它反序列化成一个 JavaScript 对象,就可以在 JavaScript 中直接使用这个对象了。
二 JSON 序列化与反序列化
说到 Json 序列化与反序列化,Gson、FastJson 和 Jackson 开源框架都很知名且使用广泛,但这几个框架主要是针对 Java 解析 Json 的,大多数采用了 Java 反射机制,注定难以适配 Kotlin 独有的特性,在使用过程中具体有以下两个问题:
- 属性声明时值不能为 null,结果反序列化后值为 null,跟预期不符
- 默认值可能不生效,可能被 null 覆盖
Moshi 是 Square 公司在 2015 年 6 月开源的有关 Json 的反序列化及序列化的框架,天生对 Kotlin 友好,而且对 Java 的解析也毫不逊色,所以不管是在 Java 跟 Kotlin 的混编还是在纯 Kotlin 项目中,Moshi 表现都很出色。
同样在 Kotlin 中,也有官方推荐的反序列化及序列化 Json 框架 kotlinx.serialization,下面简称 KS。
以下主要介绍这两种框架的用法。
2.1 Moshi
Moshi 使用 Adapter负责序列化与反序列化,每个 Kotlin 类都对应一个 Adapter,命名规则为 数据类名+JsonAdapter
,借助于内置的基本数据类型 Adapter(Int, String 等) 从而实现任意类的解析。同时也可以自定义 JsonAdapter 来处理某个类。
2.1.1 依赖
implementation( "com.squareup.moshi:moshi-kotlin:1.13.0")
KAPT 支持,用于编译期间生成 JsonAdapter(使用 Annotation 解析方式需要引入)
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
针对 Kotlin,Moshi 提供了两种解析方式,一种是通过 Reflection,一种是通过Annotation。使用 Reflection 时会在运行时通过 kotlin-reflection 反射生成自定义类的 JsonAdapter,一方面增加包大小,同时影响运行时效率;而使用 Annotation 则会在编译间生成 JsonAdapter,提升一些运行时性能。本文主要介绍第二种解析方式。
2.1.2 基本使用
首先添加注解 @JsonClass(generateAdapter=true) 标记数据类
// 标记数据类
@JsonClass(generateAdapter = true)
data class Person(
var name: String,
var age: Int,
var nationality: String="中国"
)
@JsonClass(generateAdapter = true)
标识此类,让 codegen 在编译期生成此类的 JsonAdapter,codegen 需要数据类和它的 properties 可见性都是 internal/public。
数据类序列化为 Json
// 具体使用
fun main() {
val person = Person("Lilei", 17)
// 创建 Moshi
val moshi = Moshi.Builder().build()
// 获取对应类的 adapter
val personJsonAdapter = moshi.adapter(Person::class.java)
// toJson 把对象变成 json
personJson = personJsonAdapter.toJson(json)
println(personJson)
}
Json 反序列化为数据类
fun main(){
val personJson="{\"name\":\"Lilei\",\"age\":17,\"country\":\"中国\"}"
val build = Moshi.Builder().build()
val personJsonAdapter = build.adapter<Person>(Person::class.java)
val person = personJsonAdapter.fromJson(personJson)
println(person)
}
2.1.3 自定义 JsonAdapter
Moshi 会在编译期生成我们需要的 JsonAdapter,然后通过 JsonReader 遍历的方式去解析 Json 数据,这种方式不仅仅不依赖于反射,而且速度快于 Kotlin。
JsonAdapter是 Moshi 有别于 Gson,FastJson 的最大特点,顾名思义,这是一个 Json 的转换器,他的主要作用在于将拿到的 Json 数据转换成任意你想要的类型,Moshi 内置了很多 JsonAdapter,有如下这些:
Built-in Type Adapters
- Map:MapJsonAdapter
- Enums:EnumJsonAdapter
- Arrays:ArrayJsonAdapter
- Object:ObjectJsonAdapter
- String:位于 StandardJsonAdapters,采用匿名内部类实现
- Primitives (int, float, char,boolean) :基本数据类型的 Adapter 都在 StandardJsonAdapters 里面,采用匿名内部类实现
Custom Type Adapters
对于一些比较简单规范的数据,使用 Moshi 内置的 JsonAdapter 已经完全能够满足条件,但是由于 Json 只支持基本数据类型传输,所以很多时候不能满足业务上需要,举个例子:
{
"gender": 1,
"isVIP": 1,
"phoneNumber": "MTM4ODg4ODg4ODg="
}
这是一个很普通的 Json,包含了 3 个字段,如果按照服务端返回的字段来定义解析的数据类,显然是可以完全解析的,但是我们在实际调用的时候,这些数据并不是很干净,我们还需要处理一下:
- gender:Int 类型,需要 Enum,得定义一个 Enum 的转换类,去将 Int 转换成 Enum
- isVIP:Int 类型,需要 Boolean,使用时还得将 Int 转成 Boolean
- phoneNumber:String 类型,这个字段是加密过的,可能是通过 AES 或者 RSA 加密,这里为了方便测试,只是用 Base64 对 “13888888888” 对进行 encode。
对于客户端来说,如果这种不干净 的 Json 多了之后就很头疼,每个在用的时候都需要转一遍,效率低下。而有了 Moshi 之后,我们只需要针对需要转换的类型定义对应的 JsonAdapter,达到一劳永逸的效果。
data class Person {
val gender: Gender,
val isVIP: Boolean,
val phoneNumber: String
}
此处我们定义的数据类型不是根据服务器返回的 Json 数据,而是定义的我们业务需要的格式,那么最终是通过 JsonAdapter 转换器来完成这个转换,下面开始自定义 JsonAdapter。
Int->Enum
Gender
enum class Gender {
MALE,
FEMALE
}
GenderAdapter
定义一个 GenderAdapter 继承自 JsonAdapter,传入对应的泛型,会自动帮我们复写 fromJson 跟 toJson 两个方法。
class GenderAdapter {
@FromJson
fun fromJson(value: Boolean): Gender {
return if (value) Gender.MALE else Gender.FEMALE
}
@ToJson
fun toJson(value: Gender): Boolean {
return value == Gender.MALE
}
}
至此已经完成 Gender 的转换,接下来我们再以 isVIP 举个例子,基本和 GenderAdapter 相同步骤。
Int->Boolean
BooleanAdapter
class BooleanAdapter {
@FromJson
fun fromJson(value: Int) : Boolean {
return value == 1
}
@ToJson
fun toJson(value: Boolean) : Int {
return if (value) 1 else 0
}
}
StringDecode
phoneNumberAdapter
class phoneNumberAdapter {
@FromJson
fun fromJson(value: String) : String {
val encoder: Base64.Encoder = Base64.getEncoder()
return encoder.encodeToString(value.toByteArray())
v
}
@ToJson
fun toJson(value: String) : String {
val decoder: Base64.Decoder = Base64.getDecoder()
return String(decoder.decode(value))
}
}
Adapter 测试
fun main(){
val personJson="{\"gender\":1,\"isVIP\":1,\"phoneNumber\":\"13888888888\"}"
val build = Moshi.Builder()
.add(GenderAdapter())
.add(BooleanAdapter())
.add(phoneNumberAdapter())
.build()
val personJsonAdapter = build.adapter<Person>(Person::class.java)
val person = personJsonAdapter.fromJson(personJson)
println(person)
}
2.2 kotlinx.serialization
2.2.1 依赖
项目 build.gradle
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
// 和 Kotlin 插件同一个版本号即可
module build.gradle
apply plugin: 'kotlinx-serialization'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
2.2.2 序列化
- 仅属性支持序列化(即包含 setter 和 getter)
- 默认值不会被序列化, 即使是可空属性
@Serializable
class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))
}
2.2.3 反序列化
@Serializable
class Project(val name: String, val language: String)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin")
println(Json.encodeToString(data))
}
-
私有构造函数, 暴露其他构造函数, 这样 Serialization 会序列化暴露出来的构造函数属性
-
当数据类字段比 JSON 多出属性会序列化失败, 除非多出的属性存在默认值(
@Required
则强制要求 JSON 匹配字段) -
序列化时如果存在循环结构字段, 会导致堆栈溢出, 建议你忽略排除该字段
-
当 JSON 字段覆盖属性值时, 属性值的默认值为一个函数, 该函数不会被调用
-
JSON 覆盖存在默认值的属性会错误
注解
注解 | 修饰位置 | 描述 |
---|---|---|
@Transient | 字段 | 忽略字段 |
@Required | 字段 | 强制有默认值的参数也要匹配 JSON 字段 |
@SerialName | 字段 | 修饰类为指定序列者的名称, 修饰字段为指定在 JSON 中的名称 |
@JsonNames | 字段 | 可以为字段再指定多个名称, 同时字段名也不会失效 |
@SerialInfo | 类 | 允许编译器将注释信息保存到 SerialDescriptor 中, 使用 getElementAnnotations |
@Serializer | 类 | 其指定参数为目标类, 目标类以其修饰类创建序列化器 |
三 参考文献
新一代Json解析库Moshi使用及原理解析
Moshi with Kotlin Json 库—现代化的最佳损友
Kotlin最强Json/Protobuf解析框架 - kotlin-serialization