高效的 Json 解析框架 kotlinx.serialization

SharpCJ發表於2023-12-11

一、引出問題

你是否有在使用 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

相關文章