高效的 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

從這個輸出結果,我們可以看到兩個問題:

  1. list1 經過序列化,得到的 jsonString 中, gender 屬性是空。
  2. jsonString 反序列化過程中發生了異常。

二、解決問題

異常信息已經指明瞭問題的解決方案

Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this type.

抽象類無法實例化!爲此類型註冊 InstanceCreator 或 TypeAdapter。

其實也很好理解。 sealed classabstract classinterface 都是抽象的,不能直接被實例化。對於抽象類的子類或者接口的實現類,應該明確制定序列化和反序列化的規則。由於我們沒有註冊 TypeAdapter, 默認的 TypeAdapter ,將 Gender 屬性序列化爲了空對象。在進行反序列化時,空對象不知道應該如何反序列化,所以拋出瞭如下的異常。

解決辦法之一,在序列化和反序列化時,需要使用 Gson 的 registerTypeAdapterregisterTypeHierarchyAdapter 方法來處理密封類的子類。

首先爲抽象類/接口創建一個 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, 沒有問題。
那... registerTypeAdapterregisterTypeHierarchyAdapter 兩個方法有什麼區別呢?

它們的主要區別在於註冊對象的範圍不同。

  • 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 的使用

  1. plugins 引入:
plugins {
    id("org.jetbrains.kotlin.plugin.serialization") version("1.4.30")
}
  1. dependencies 引入:
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
  1. 通過添加 @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 具有如下特性:

  1. 類型安全:滿足 Kotlin 的強制類型安全。可處理 Kotlin 的可空類型。
  2. 支持屬性默認值:解析 JSON 的時候支持 Kotlin 類中屬性的默認值。
  3. 支持泛型類型:API在序列化和反序列化泛型類型的時候非常簡單也非常高效。
  4. 序列化字段名:當 json 的 key 和字段名不一致時,可以通過 @SerialName 給字段進行序列化。 同 Gson 中的 @SerializedName
  5. 序列化引用對象:當屬性的類型是引用類型時,對該類型也需要使用 @Serializable 註解。
  6. 數據校驗:可以再 json 反序列化時對數據進行校驗。
  7. 支持 Retrofit 庫。詳見針對Retrofit 2 Converter.Factory的Kotlin序列化的庫。

kotlinx.serialization 有很多優秀的特性。本文算是拋磚引玉。更多特性,請自己手動 Coding 體驗。
最後附上 kotlinx.serialization 的官方文檔:https://github.com/Kotlin/kotlinx.serialization

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章