首页 前端知识 Kotlin 中的 JSON 序列化与反序列化

Kotlin 中的 JSON 序列化与反序列化

2024-06-16 01:06:53 前端知识 前端哥 5 241 我要收藏

一 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 的 truefalse
  • 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

转载请注明出处或者链接地址:https://www.qianduange.cn//article/12280.html
标签
kotlin
评论
发布的文章

json文件的格式转换

2024-06-21 09:06:48

JSON 现代数据交换的利器

2024-06-21 09:06:41

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!