【kotlin】Serialization Basic 要將物件樹轉換成字串或位元組序列,必須經過兩個相互交織的過程。 第一步是序列化——將物件轉換成其組成的基本值的序列。這個過程對所有資料格式來說都是 共通的,結果取決於被序列化的物件。序列化的過程由序列化器控制。 第二步稱為編碼——這是將相應的基本值序列轉換成輸出格式表示的過程。編碼器控制這個過程。當區分不那麼重要時,編碼和序列化這兩個術語可以互換使用。 +---------+ 序列化 +------------+ 編碼 +---------------+ | 物件 | ------------> | 基本值 | ---------> | 輸出格式 | +---------+ +------------+ +---------------+  +---------+ Serialization +------------+ Encoding +---------------+ | Objects | --------------> | Primitives | ---------> | Output format | +---------+ +------------+ +---------------+ 反向過程從解析輸入格式並解碼基本值開始,隨後將得到的流反序列化為物件。我們稍後會詳細探討這個過程。 現在,我們從 JSON 編碼開始。 JSON 編碼 將資料轉換成特定格式的整個過程稱為編碼。對於 JSON,我們使用  Json.encodeToString 擴展函數來編碼資料。它在底層將傳遞給它的物件序列化並編碼為 JSON 字串。 讓我們從描述專案的一個類別開始,並嘗試獲得其 JSON 表示。 class Project(val name: String, val language: String) fun main() { val data = Project("kotlinx.serialization", "Kotlin") println(Json.encodeToString(data)) } 執行這段程式碼時,我們會得到一個例外。 Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Project' is not found. Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied. 可序列化的類別必須顯式標記。Kotlin Serialization 不使用反射,因此您無法意外反序列化不應該可序列化的類別。我們通過添加  @Serializable 註解來修正它。 @Serializable class Project(val name: String, val language: String) fun main() { val data = Project("kotlinx.serialization", "Kotlin") println(Json.encodeToString(data)) } 這樣,我們將會得到相應的 JSON 輸出。 {"name":"kotlinx.serialization","language":"Kotlin"} JSON 解碼 反向過程稱為解碼。要將 JSON 字串解碼為物件,我們將使用  Json.decodeFromString 擴展函數。為了指定我們想要獲得的結果類型,我們將類型參數提供給這個函數。 如我們稍後所見,序列化可以處理不同種類的類別。這裡我們將  Project 類別標記為  data class,這不是因為它是必需的,而是因為我們想要打印其內容以驗證它是如何解碼的。 @Serializable data class Project(val name: String, val language: String) fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization","language":"Kotlin"} """) println(data) } 執行這段程式碼後,我們會得到物件: Project(name=kotlinx.serialization, language=Kotlin) 可序列化的類別 這部分將更詳細地說明如何處理不同的  @Serializable 類別。 序列化備援屬性 只有具有備援欄位的類別屬性會被序列化,因此沒有備援欄位的 getter/setter 屬性和委託屬性將不會被序列化,如以下範例所示。 @Serializable class Project( var name: String // name 是具有備援欄位的屬性——會被序列化 ) { var stars: Int = 0 // 具有備援欄位的屬性——會被序列化 val path: String // 僅有 getter,沒有備援欄位——不會被序列化 get() = "kotlin/$name" var id by ::name // 委託屬性——不會被序列化 } fun main() { val data = Project("kotlinx.serialization").apply { stars = 9000 } println(Json.encodeToString(data)) } 我們可以清楚地看到,只有  name 和  stars 屬性出現在 JSON 輸出中。 {"name":"kotlinx.serialization","stars":9000} 建構子屬性要求 如果我們想要定義一個  Project 類別,並讓它接受一個路徑字串,然後將其分解為相應的屬性,我們可能會傾向於寫出如下代碼。 @Serializable class Project(path: String) { val owner: String = path.substringBefore('/') val name: String = path.substringAfter('/') } 這個類別無法編譯,因為  @Serializable 註解要求類別主要建構子的所有參數必須是屬性。一個簡單的解決方案是定義一個具有屬性的私有主要建構子,並將我們想要的建構子變成次要建構子。 @Serializable class Project private constructor(val owner: String, val name: String) { constructor(path: String) : this( owner = path.substringBefore('/'), name = path.substringAfter('/') ) val path: String get() = "$owner/$name" } 序列化在具有私有主要建構子的情況下仍然能夠正常工作,並且仍然只會序列化備援欄位。 fun main() { println(Json.encodeToString(Project("kotlin/kotlinx.serialization"))) } 這個範例會產生預期的輸出。 {"owner":"kotlin","name":"kotlinx.serialization"} 資料驗證 當你希望在將值存儲到屬性之前進行驗證時,可能會想在主要建構子參數中引入一個沒有屬性的參數。為了使其可序列化,你應該將其替換為主要建構子中的屬性,並將驗證移至  init { ... } 區塊。 @Serializable class Project(val name: String) { init { require(name.isNotEmpty()) { "name 不能為空" } } } 反序列化過程與 Kotlin 中的常規建構子一樣,會調用所有的  init 區塊,確保你無法通過反序列化獲得一個無效的類別。讓我們試試看。 fun main() { val data = Json.decodeFromString(""" {"name":""} """) println(data) } 執行這段程式碼會產生一個例外: Exception in thread "main" java.lang.IllegalArgumentException: name 不能為空 選擇性屬性 只有當輸入中存在所有屬性時,物件才能被反序列化。例如,執行以下程式碼。 @Serializable data class Project(val name: String, val language: String) fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization"} """) println(data) } 這會產生一個例外: Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses04.Project', but it was missing at path: $ 這個問題可以通過為屬性添加一個默認值來解決,這會自動使其在序列化中成為選擇性屬性。 @Serializable data class Project(val name: String, val language: String = "Kotlin") fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization"} """) println(data) } 這會產生以下輸出,其中  language 屬性使用默認值。 Project(name=kotlinx.serialization, language=Kotlin) 選擇性屬性初始化呼叫 當選擇性屬性存在於輸入中時,對應屬性的初始化器 甚至不會被調用。這是一個為了性能而設計的特性,所以要小心不要依賴於初始化器中的副作用。考慮以下範例。 fun computeLanguage(): String { println("計算中") return "Kotlin" } @Serializable data class Project(val name: String, val language: String = computeLanguage()) fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization","language":"Kotlin"} """) println(data) } 由於輸入中指定了  language 屬性,因此我們不會看到 "計算中" 的字串被打印出來。 Project(name=kotlinx.serialization, language=Kotlin) 必須的屬性 具有默認值的屬性可以通過  @Required 註解在序列格式中設置為必須的。我們將上一個範例的  language 屬性標記為  @Required。 @Serializable data class Project(val name: String, @Required val language: String = "Kotlin") fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization"} """) println(data) } 我們會得到以下例外。 Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'language' is required for type with serial name 'example.exampleClasses07.Project', but it was missing at path: $ 暫態屬性 屬性可以通過  @Transient 註解排除在序列化之外(不要將其與  kotlin.jvm.Transient 混淆)。暫態屬性必須有一個默認值。 @Serializable data class Project(val name: String, @Transient val language: String = "Kotlin") fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization","language":"Kotlin"} """) println(data) } 即使在序列格式中明確指定了它的值,即使指定的值與默認值相等,也會產生以下例外。 Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $.name Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys. 忽略未知鍵功能在 "忽略未知鍵" 一節中解釋。 默認值不會被編碼 默認情況下,默認值不會在 JSON 中編碼。這種行為的動機是,在大多數現實生活場景中,這種配置減少了視覺雜訊,並節省了序列化的資料量。 @Serializable data class Project(val name: String, val language: String = "Kotlin") fun main() { val data = Project("kotlinx.serialization") println(Json.encodeToString(data)) } 這會產生以下輸出,因為  language 屬性的值等於默認值,所以它不會出現在 JSON 中。 {"name":"kotlinx.serialization"} 查看 JSON 的 "編碼默認值" 一節了解如何配置此行為。此外,這種行為可以在不考慮格式設置的情況下進行控制。為此,可以使用  EncodeDefault 註解: @Serializable data class Project( val name: String, @EncodeDefault val language: String = "Kotlin" ) 這個註解指示框架無論值或格式設置如何,都要始終序列化屬性。還可以使用  EncodeDefault.Mode 參數將其調整為相反的行為: @Serializable data class User( val name: String, @EncodeDefault(EncodeDefault.Mode.NEVER) val projects: List = emptyList() ) fun main() { val userA = User("Alice", listOf(Project("kotlinx.serialization"))) val userB = User("Bob") println(Json.encodeToString(userA)) println(Json.encodeToString(userB)) } 如您所見, language 屬性被保留,而  projects 被忽略: {"name":"Alice","projects":[{"name":"kotlinx.serialization","language":"Kotlin"}]} {"name":"Bob"} 可空屬性 Kotlin Serialization 原生支持可空屬性。 @Serializable class Project(val name: String, val renamedTo: String? = null) fun main() { val data = Project("kotlinx.serialization") println(Json.encodeToString(data)) } 由於默認值不會被編碼,因此此範例不會在 JSON 中編碼 null。 {"name":"kotlinx.serialization"} 強制型別安全 Kotlin Serialization 強制執行 Kotlin 編程語言的型別安全。特別是,讓我們嘗試將 JSON 對象中的 null 值解碼為非空的 Kotlin 屬性  language。 @Serializable data class Project(val name: String, val language: String = "Kotlin") fun main() { val data = Json.decodeFromString(""" {"name":"kotlinx.serialization","language":null} """) println(data) } 即使  language 屬性具有默認值,但將 null 值分配給它仍然是一個錯誤。 Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 52: Expected string literal but 'null' literal was found at path: $.language Use 'coerceInputValues = true' in 'Json {}' builder to coerce nulls if property has a default value. 在解碼第三方 JSON 時,可能希望將 null 強制為默認值。相應的功能在 "強制輸入值" 一節中解釋。 引用物件 可序列化的類別可以在其可序列化屬性中引用其他類別。引用的類別也必須標記為  @Serializable。 @Serializable class Project(val name: String, val owner: User) @Serializable class User(val name: String) fun main() { val owner = User("kotlin") val data = Project("kotlinx.serialization", owner) println(Json.encodeToString(data)) } 當編碼為 JSON 時,它會生成一個嵌套的 JSON 物件。 {"name":"kotlinx.serialization","owner":{"name":"kotlin"}} 對於不可序列化的類別引用,可以將其標記為暫態屬性,或者為它們提供自定義序列化器,如 "序列化器" 一章所示。 不壓縮重複引用 Kotlin Serialization 專為編碼和解碼純資料而設計。它不支持重建具有重複物件引用的任意物件圖。例如,讓我們嘗試序列化一個兩次引用同一  owner 實例的物件。 @Serializable class Project(val name: String, val owner: User, val maintainer: User) @Serializable class User(val name: String) fun main() { val owner = User("kotlin") val data = Project("kotlinx.serialization", owner, owner) println(Json.encodeToString(data)) } 我們會得到兩次編碼的  owner 值。 {"name":"kotlinx.serialization","owner":{"name":"kotlin"},"maintainer":{"name":"kotlin"}} 嘗試序列化循環結構會導致堆疊溢出。你可以使用暫態屬性來排除某些引用的序列化。 泛型類別 Kotlin 中的泛型類別提供了型別多態行為,這在編譯期間由 Kotlin Serialization 強制執行。例如,考慮一個泛型可序列化類別  Box。 @Serializable class Box(val contents: T) Box 類別可以與內建型別如  Int 一起使用,也可以與使用者定義的型別如  Project 一起使用。 @Serializable class Data( val a: Box, val b: Box ) fun main() { val data = Data(Box(42), Box(Project("kotlinx.serialization", "Kotlin"))) println(Json.encodeToString(data)) } 我們在 JSON 中獲得的實際型別取決於為  Box 指定的實際編譯時型別參數。 {"a":{"contents":42},"b":{"contents":{"name":"kotlinx.serialization","language":"K otlin"}}} 如果實際泛型類型不可序列化,則會產生編譯時錯誤。 序列欄位名稱 在編碼表示中使用的屬性名稱(例如我們的 JSON 示例中的屬性名稱)默認情況下與其在源代碼中的名稱相同。用於序列化的名稱稱為序列名稱,可以使用  @SerialName 註解進行更改。例如,我們可以在源代碼中有一個  language 屬性,並將其序列名稱縮寫。 @Serializable class Project(val name: String, @SerialName("lang") val language: String) fun main() { val data = Project("kotlinx.serialization", "Kotlin") println(Json.encodeToString(data)) } 現在我們看到在 JSON 輸出中使用了縮寫名稱  lang。 {"name":"kotlinx.serialization","lang":"Kotlin"} 下一章將介紹內建類別。   標準內建類別 這是《Kotlin 序列化指南》的第二章。除了所有的基本類型和字串外,Kotlin 標準函式庫中的一些類別(包括標準集合)的序列化功能也內建在 Kotlin 序列化中。本章將解釋這些功能的詳細內容。 目錄 基本類型 數字 長整數 作為字串的長整數 列舉類別 列舉項目的序列名稱 複合類型 Pair 和  Triple 列表 集合和其他集合類型 反序列化集合 映射 ( Map) 單例物件 ( Unit) 和單例物件類別 持續時間 ( Duration) Nothing 基本類型 Kotlin 序列化支援以下十種基本類型: Boolean、 Byte、 Short、 Int、 Long、 Float、 Double、 Char、 String 以及列舉類型。Kotlin 序列化中的其他類型都是由這些基本值組成的複合類型。 數字 所有類型的整數和浮點數 Kotlin 數字都可以序列化。 @Serializable class Data( val answer: Int, val pi: Double ) fun main() { val data = Data(42, PI) println(Json.encodeToString(data)) } 這些數字在 JSON 中會被表示為自然的形式。 {"answer":42,"pi":3.141592653589793} 長整數 長整數 ( Long) 也可以被序列化。 @Serializable class Data(val signature: Long) fun main() { val data = Data(0x1CAFE2FEED0BABE0) println(Json.encodeToString(data)) } 預設情況下,它們會被序列化為 JSON 數字。 {"signature":2067120338512882656} 作為字串的長整數 上面的 JSON 輸出會被 Kotlin/JS 上運行的 Kotlin 序列化正常解碼。然而,如果我們嘗試使用原生的 JavaScript 方法解析這個 JSON,會得到這樣的截斷結果。 JSON.parse("{\"signature\":2067120338512882656}") // ▶ {signature: 2067120338512882700} Kotlin 的  Long 類型的完整範圍無法適配 JavaScript 的數字,因此在 JavaScript 中會丟失精度。常見的解決方案是使用 JSON 字串類型來表示具有完整精度的長整數。Kotlin 序列化通過  LongAsStringSerializer 選擇性地支持這種方法,可以使用  @Serializable 註解將其指定給特定的  Long 屬性: @Serializable class Data( @Serializable(with=LongAsStringSerializer::class) val signature: Long ) fun main() { val data = Data(0x1CAFE2FEED0BABE0) println(Json.encodeToString(data)) } 這個 JSON 可以被 JavaScript 原生解析而不丟失精度。 {"signature":"2067120338512882656"} 關於如何為整個檔案中的所有屬性指定類似  LongAsStringSerializer 的序列化器,可以參考 "為檔案指定序列化器" 這一節。 列舉類別 所有的列舉類別都是可序列化的,無需將它們標記為  @Serializable,如下例所示。 // 列舉類別不需要 @Serializable 註解 enum class Status { SUPPORTED } @Serializable class Project(val name: String, val status: Status) fun main() { val data = Project("kotlinx.serialization", Status.SUPPORTED) println(Json.encodeToString(data)) } 在 JSON 中,列舉會被編碼為字串。 {"name":"kotlinx.serialization","status":"SUPPORTED"} 注意:在 Kotlin/JS 和 Kotlin/Native 上,如果你想將列舉類別作為根物件使用(即使用  encodeToString(Status.SUPPORTED)),需要為列舉類別添加  @Serializable 註解。 列舉項目的序列名稱 列舉項目的序列名稱可以像 "序列欄位名稱" 一節所示一樣,通過  SerialName 註解來自訂。然而,在這種情況下,整個列舉類別必須標記為  @Serializable。 @Serializable // 因為使用了 @SerialName,所以需要此註解 enum class Status { @SerialName("maintained") SUPPORTED } @Serializable class Project(val name: String, val status: Status) fun main() { val data = Project("kotlinx.serialization", Status.SUPPORTED) println(Json.encodeToString(data)) } 我們可以看到,在結果 JSON 中使用了指定的序列名稱。 {"name":"kotlinx.serialization","status":"maintained"} 複合類型 Kotlin 序列化支援標準函式庫中的一些複合類型。 Pair 和  Triple Kotlin 標準函式庫中的簡單資料類別  Pair 和  Triple 是可序列化的。 @Serializable class Project(val name: String) fun main() { val pair = 1 to Project("kotlinx.serialization") println(Json.encodeToString(pair)) } {"first":1,"second":{"name":"kotlinx.serialization"}} 並不是所有的 Kotlin 標準函式庫中的類別都是可序列化的,特別是範圍 ( Range) 和正則表達式 ( Regex) 類別目前不可序列化。未來可能會添加它們的序列化支持。 列表 可以序列化一個可序列化類別的列表。 @Serializable class Project(val name: String) fun main() { val list = listOf( Project("kotlinx.serialization"), Project("kotlinx.coroutines") ) println(Json.encodeToString(list)) } 結果在 JSON 中表示為列表。 [{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}] 集合和其他集合類型 其他集合類型,如集合 ( Set),也可以被序列化。 @Serializable class Project(val name: String) fun main() { val set = setOf( Project("kotlinx.serialization"), Project("kotlinx.coroutines") ) println(Json.encodeToString(set)) } Set 也會像其他所有集合一樣,在 JSON 中表示為列表。 [{"name":"kotlinx.serialization"},{"name":"kotlinx.coroutines"}] 反序列化集合 在反序列化過程中,結果物件的類型由源代碼中指定的靜態類型決定——無論是屬性的類型還是解碼函數的類型參數。以下範例展示了相同的 JSON 整數列表如何被反序列化為兩個不同 Kotlin 類型的屬性。 @Serializable data class Data( val a: List, val b: Set ) fun main() { val data = Json.decodeFromString(""" { "a": [42, 42], "b": [42, 42] } """) println(data) } 由於  data.b 屬性是  Set,因此其中的重複值消失了。 Data(a=[42, 42], b=[42]) 映射 ( Map) 具有基本類型或列舉鍵和任意可序列化值的  Map 可以被序列化。 @Serializable class Project(val name: String) fun main() { val map = mapOf( 1 to Project("kotlinx.serialization"), 2 to Project("kotlinx.coroutines") ) println(Json.encodeToString(map)) } Kotlin 的映射在 JSON 中表示為物件。在 JSON 中,物件的鍵總是字串,所以即使在 Kotlin 中它們是數字,也會被編碼為字串,如下所示。 {"1":{"name":"kotlinx.serialization"},"2":{"name":"kotlinx.coroutines"}} 這是 JSON 的 一個特定限制,即鍵不能是複合的。可以通過參考 "允許結構化的映射鍵" 這一節來解決這個限制。 單例物件 ( Unit) 和單例物件類別 Kotlin 內建的  Unit 類型也是可序列化的。 Unit 是 Kotlin 的一個單例物件,並且與其他 Kotlin 物件一樣處理。 從概念上講,單例物件是一個只有一個實例的類別,這意味著狀態不定義物件,而是物件定義其狀態。在 JSON 中,物件會被序列化為空結構。 @Serializable object SerializationVersion { val libraryVersion: String = "1.0.0" } fun main() { println(Json.encodeToString(SerializationVersion)) println(Json.encodeToString(Unit)) } 雖然這在表面上看似無用,但在密封類別序列化中這很有用,這在 "多型性" 章節的 "物件" 部分中進行了解釋。 {} {} 物件的序列化是格式特定的。其他格式可能會以不同方式表示物件,例如使用它們的全名。 持續時間 ( Duration) 自 Kotlin 1.7.20 以來, Duration 類別已經成為可序列化的。 fun main() { val duration = 1000.toDuration(DurationUnit.SECONDS) println(Json.encodeToString(duration)) } Duration 會被序列化為 ISO-8601-2 格式的字串。 "PT16M40S" Nothing 預設情況下, Nothing 是一個可序列化的類別。然而,由於該類別沒有實例,因此不可能對其值進行編碼或解碼——任何嘗試都會導致異常。 當語法上需要某種類型,但實際上在序列化中不使用時,會使用這個序列化器。例如,在使用參數化多型基類時: @Serializable sealed class ParametrizedParent { @Serializable data class ChildWithoutParameter(val value: Int) : ParametrizedParent() } fun main() { println(Json.encodeToString(ParametrizedParent.ChildWithoutParameter(42))) } 在編碼過程中, Nothing 的序列化器未被使用。 {"value":42} 下一章將介紹序列化器 ( Serializers)。 序列化器 這是《Kotlin 序列化指南》的第三章。本章將更詳細地介紹序列化器,並展示如何編寫自定義序列化器。 目錄 序列化器介紹 插件生成的序列化器 插件生成的泛型序列化器 內建的基本序列化器 構建集合序列化器 使用頂級序列化器函數 自定義序列化器 基本序列化器 委派序列化器 通過代理實現複合序列化器 手寫的複合序列化器 順序解碼協議(實驗性) 序列化第三方類別 手動傳遞序列化器 為屬性指定序列化器 為特定類型指定序列化器 為檔案指定序列化器 使用型別別名全局指定序列化器 泛型類型的自定義序列化器 特定格式的序列化器 上下文序列化 序列化器模組 上下文序列化與泛型類別 為其他 Kotlin 類別衍生外部序列化器(實驗性) 外部序列化使用屬性 序列化器介紹 像 JSON 這樣的格式控制著物件編碼成特定的輸出位元組,但如何將物件分解為其組成屬性則由序列化器控制。到目前為止,我們已經使用了通過  @Serializable 註解自動生成的序列化器,如 "可序列化的類別" 章節中所解釋的,或使用了在 "內建類別" 章節中展示的內建序列化器。 作為一個激勵性的例子,讓我們來看一下下面這個  Color 類別,它使用一個整數值來存儲其 RGB 位元組。 @Serializable class Color(val rgb: Int) fun main() { val green = Color(0x00ff00) println(Json.encodeToString(green)) } 預設情況下,這個類別會將其  rgb 屬性序列化為 JSON。 {"rgb":65280} 插件生成的序列化器 每個標記有  @Serializable 註解的類別(如上一個例子中的  Color 類別)都會由 Kotlin 序列化編譯器插件自動生成一個  KSerializer 介面的實例。我們可以使用類別的伴生對象上的  .serializer() 函數來檢索此實例。 我們可以檢查其  descriptor 屬性,它描述了序列化類別的結構。我們會在後續章節中詳細了解這一點。 fun main() { val colorSerializer: KSerializer = Color.serializer() println(colorSerializer.descriptor) } 輸出: Color(rgb: kotlin.Int) 當  Color 類別本身被序列化時,或當它被用作其他類別的屬性時,Kotlin 序列化框架會自動檢索並使用此序列化器。 你無法在可序列化類別的伴生對象上自定義自己的  serializer() 函數。 插件生成的泛型序列化器 對於泛型類別,如 "泛型類別" 章節中展示的  Box 類別,自動生成的  .serializer() 函數接受的參數數量與對應類別中的類型參數數量相同。這些參數的類型是  KSerializer,因此在構建泛型類別的序列化器實例時,必須提供實際類型參數的序列化器。 @Serializable @SerialName("Box") class Box(val contents: T) fun main() { val boxedColorSerializer = Box.serializer(Color.serializer()) println(boxedColorSerializer.descriptor) } 我們可以看到,已經實例化了一個序列化器來序列化具體的  Box。 Box(contents: Color) 內建的基本序列化器 內建類別的基本序列化器可以通過  .serializer() 擴展函數來檢索。 fun main() { val intSerializer: KSerializer = Int.serializer() println(intSerializer.descriptor) } 構建集合序列化器 當需要時,內建集合的序列化器必須通過對應的函數如  ListSerializer()、 SetSerializer()、 MapSerializer() 等顯式構建。這些類別是泛型的,因此要實例化它們的序列化器,我們必須為其類型參數提供相應的序列化器。例如,我們可以如下生成一個  List 的序列化器。 fun main() { val stringListSerializer: KSerializer> = ListSerializer(String.serializer()) println(stringListSerializer.descriptor) } 使用頂級序列化器函數 當不確定時,你可以隨時使用頂級泛型  serializer() 函數來檢索源代碼中任意 Kotlin 類型的序列化器。 @Serializable @SerialName("Color") class Color(val rgb: Int) fun main() { val stringToColorMapSerializer: KSerializer> = serializer() println(stringToColorMapSerializer.descriptor) } 自定義序列化器 插件生成的序列化器非常方便,但對於像  Color 這樣的類別,它可能無法生成我們想要的 JSON。讓我們來研究一些替代方案。 基本序列化器 我們想將  Color 類別序列化為一個十六進制字串,其中綠色將被表示為  "00ff00"。為了實現這一點,我們需要編寫一個實現  KSerializer 介面的物件來處理  Color 類別。 object ColorAsStringSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Color) { val string = value.rgb.toString(16).padStart(6, '0') encoder.encodeString(string) } override fun deserialize(decoder: Decoder): Color { val string = decoder.decodeString() return Color(string.toInt(16)) } } 序列化器包含三個必須的部分: serialize 函數實現了  SerializationStrategy。它接收一個  Encoder 的實例和一個要序列化的值。它使用  Encoder 的  encodeXxx 函數來將值表示為一系列基本類型。在我們的例子中,使用了  encodeString。 deserialize 函數實現了  DeserializationStrategy。它接收一個  Decoder 的實例並返回一個反序列化的值。它使用  Decoder 的  decodeXxx 函數來解碼對應的值。在我們的例子中,使用了  decodeString。 descriptor 屬性必須準確描述  encodeXxx 和  decodeXxx 函數的作用,以便格式實現能夠提前知道它們將調用哪些編碼/解碼方法。對於基本序列化,必須使用  PrimitiveSerialDescriptor 函數並為正在序列化的類型提供唯一的名稱。 PrimitiveKind 描述了實現中使用的特定  encodeXxx/decodeXxx 方法。 當  descriptor 與編碼/解碼方法不對應時,結果代碼的行為是未定義的,可能會在未來的更新中隨機更改。 下一步是將序列化器綁定到類別。這可以通過在  @Serializable 註解中添加  with 屬性來實現。 @Serializable(with = ColorAsStringSerializer::class) class Color(val rgb: Int) 現在我們可以像以前一樣序列化  Color 類別了。 fun main() { val green = Color(0x00ff00) println(Json.encodeToString(green)) } 我們得到了我們想要的十六進制字串 的序列表示。 "00ff00" 反序列化也很簡單,因為我們實現了  deserialize 方法。 @Serializable(with = ColorAsStringSerializer::class) class Color(val rgb: Int) fun main() { val color = Json.decodeFromString("\"00ff00\"") println(color.rgb) // prints 65280 } 它也適用於我們序列化或反序列化具有  Color 屬性的不同類別。 @Serializable(with = ColorAsStringSerializer::class) data class Color(val rgb: Int) @Serializable data class Settings(val background: Color, val foreground: Color) fun main() { val data = Settings(Color(0xffffff), Color(0)) val string = Json.encodeToString(data) println(string) require(Json.decodeFromString(string) == data) } 兩個  Color 屬性都被序列化為字串。 {"background":"ffffff","foreground":"000000"} 委派序列化器 在前面的例子中,我們將  Color 類別表示為字串。字串被認為是一種基本類型,因此我們使用了  PrimitiveClassDescriptor 和專門的  encodeString 方法。現在讓我們看看如果我們需要將  Color 序列化為另一種非基本類型(例如  IntArray),我們的操作會是什麼。 KSerializer 的實現將在  Color 和  IntArray 之間進行轉換,但實際的序列化邏輯將委派給  IntArraySerializer,使用  encodeSerializableValue 和  decodeSerializableValue。 import kotlinx.serialization.builtins.IntArraySerializer class ColorIntArraySerializer : KSerializer { private val delegateSerializer = IntArraySerializer() override val descriptor = SerialDescriptor("Color", delegateSerializer.descriptor) override fun serialize(encoder: Encoder, value: Color) { val data = intArrayOf( (value.rgb shr 16) and 0xFF, (value.rgb shr 8) and 0xFF, value.rgb and 0xFF ) encoder.encodeSerializableValue(delegateSerializer, data) } override fun deserialize(decoder: Decoder): Color { val array = decoder.decodeSerializableValue(delegateSerializer) return Color((array[0] shl 16) or (array[1] shl 8) or array[2]) } } 注意,這裡我們不能使用預設的  Color.serializer().descriptor,因為依賴於架構的格式可能會認為我們會調用  encodeInt 而不是  encodeSerializableValue。同樣,我們也不能直接使用  IntArraySerializer().descriptor,否則處理整數數組的格式將無法區分值是  IntArray 還是  Color。不用擔心,當序列化實際的底層整數數組時,這個優化仍然會起作用。 我們現在可以使用這個序列化器: @Serializable(with = ColorIntArraySerializer::class) class Color(val rgb: Int) fun main() { val green = Color(0x00ff00) println(Json.encodeToString(green)) } 正如您所見,這種數組表示在 JSON 中不是很有用,但在與  ByteArray 和二進制格式一起使用時可能會節省一些空間。 [0,255,0] 通過代理實現複合序列化器 現在我們的挑戰是讓  Color 被序列化,這樣它在 JSON 中被表示為一個具有三個屬性( r、 g 和  b)的類別,從而讓 JSON 將其編碼為物件。實現這一目標的最簡單方法是定義一個模擬  Color 序列化形式的代理類別,然後將其用作  Color 的序列化器。我們還可以將這個代理類別的  SerialName 設置為  Color。然後,如果任何格式使用此名稱,代理看起來就像是一個  Color 類別。代理類別可以是私有的,並且可以在其  init 區塊中強制執行所有關於類別序列表示的約束。 @Serializable @SerialName("Color") private class ColorSurrogate(val r: Int, val g: Int, val b: Int) { init { require(r in 0..255 && g in 0..255 && b in 0..255) } } 現在我們可以使用  ColorSurrogate.serializer() 函數來檢索為代理類別自動生成的序列化器。 我們可以像在委派序列化器中那樣使用相同的方法,但這次,我們完全重用了為代理類別自動生成的  SerialDescriptor,因為它應該與原始類別無法區分。 object ColorSerializer : KSerializer { override val descriptor: SerialDescriptor = ColorSurrogate.serializer().descriptor override fun serialize(encoder: Encoder, value: Color) { val surrogate = ColorSurrogate((value.rgb shr 16) and 0xff, (value.rgb shr 8) and 0xff, value.rgb and 0xff) encoder.encodeSerializableValue(ColorSurrogate.serializer(), surrogate) } override fun deserialize(decoder: Decoder): Color { val surrogate = decoder.decodeSerializableValue(ColorSurrogate.serializer()) return Color((surrogate.r shl 16) or (surrogate.g shl 8) or surrogate.b) } } 我們將  ColorSerializer 序列化器綁定到  Color 類別。 @Serializable(with = ColorSerializer::class) class Color(val rgb: Int) 現在我們可以享受  Color 類別序列化的結果了。 {"r":0,"g":255,"b":0} 手寫的複合序列化器 有些情況下,代理解決方案並不適用。可能我們想避免額外分配的性能影響,或者我們希望為最終的序列化表示提供一組可配置/動態屬性。在這些情況下,我們需要手動編寫一個模仿生成的序列化器行為的類別序列化器。 object ColorAsObjectSerializer : KSerializer { 讓我們逐步介紹它。首先,使用  buildClassSerialDescriptor 構建器定義一個  descriptor。在構建器的 DSL 中, element 函數會根據其類型自動檢索對應字段的序列化器。元素的順序很重要。它們從零開始編號。 override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Color") { element("r") element("g") element("b") } 這裡的 "element" 是一個通用術語。 descriptor 的元素取決於其  SerialKind。類別描述符的元素是其屬性,枚舉描述符的元素是其案例,等等。 接著,我們使用  encodeStructure DSL 編寫  serialize 函數,它在其區塊中提供對  CompositeEncoder 的訪問。 Encoder 和  CompositeEncoder 之間的區別在於後者具有對應於前者的  encodeXxx 函數的  encodeXxxElement 函數。它們必須按與  descriptor 中相同的順序調用。 override fun serialize(encoder: Encoder, value: Color) = encoder.encodeStructure(descriptor) { encodeIntElement(descriptor, 0, (value.rgb shr 16) and 0xff) encodeIntElement(descriptor, 1, (value.rgb shr 8) and 0xff) encodeIntElement(descriptor, 2, value.rgb and 0xff) } 最複雜的部分是  deserialize 函數。它必須支持像 JSON 這樣可以以任意順序解碼屬性的格式。它以調用  decodeStructure 開始,從而獲得  CompositeDecoder 的訪問權限。在內部,我們編寫了一個循環,該循環反覆調用  decodeElementIndex 來解碼下一個元素的索引,然後使用我們的示例中的  decodeIntElement 解碼相應的元素,最後當遇到  CompositeDecoder.DECODE_DONE 時終止循環。 override fun deserialize(decoder: Decoder): Color = decoder.decodeStructure(descriptor) { var r = -1 var g = -1 var b = -1 while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> r = decodeIntElement(descriptor, 0) 1 -> g = decodeIntElement(descriptor, 1) 2 -> b = decodeIntElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } require(r in 0..255 && g in 0..255 && b in 0..255) Color((r shl 16) or (g shl 8) or b) } 現在,我們將最終的序列化器綁定到  Color 類別並測試其序列化/反序列化。 @Serializable(with = ColorAsObjectSerializer::class) data class Color(val rgb: Int) fun main() { val color = Color(0x00ff00) val string = Json.encodeToString(color) println(string) require(Json.decodeFromString(string) == color) } 與之前一樣,我們得到了以具有三個鍵的 JSON 物件表示的  Color 類別: {"r":0,"g":255,"b":0} 順序解碼協議(實驗性) 前一部分中的  deserialize 函數的實現適用於任何格式。然而,有些格式無論何時都會按順序存儲所有複雜數據,或者有時這樣做(例如,JSON 始終按順序存儲集合)。對於這些格式來說,在循環中調用  decodeElementIndex 的複雜協議並不是必需的,如果  CompositeDecoder.decodeSequentially 函數返回  true,則可以使用更快的實現。插件生成的序列化器實際上在概念上類似於以下代碼。 override fun deserialize(decoder: Decoder): Color = decoder.decodeStructure(descriptor) { var r = -1 var g = -1 var b = -1 if (decodeSequentially()) { // sequential decoding protocol r = decodeIntElement(descriptor, 0) g = decodeIntElement(descriptor, 1) b = decodeIntElement(descriptor, 2) } else while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> r = decodeIntElement(descriptor, 0) 1 -> g = decodeIntElement(descriptor, 1) 2 -> b = decodeIntElement(descriptor, 2) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index: $index") } } require(r in 0..255 && g in 0..255 && b in 0..255) Color((r shl 16) or (g shl 8) or b) } 序列化第三方類別 有時應用程式必須處理一個不可序列化的外部類型。我們以  java.util.Date 為例。如前所述,我們從為該類別編寫  KSerializer 的實現開始。我們的目標是將  Date 序列化為一個長整數,表示自 Unix 紀元以來的毫秒數,這與 "基本序列化器" 章節中的方法類似。 在接下來的章節中,任何類型的  Date 序列化器都可以工作。例如,如果我們希望將  Date 序列化為一個物件,我們可以使用 "通過代理實現複合序列化器" 章節中的方法。 如果您需要序列化一個本應可序列化但實際不可序列化的第三方 Kotlin 類別,請參閱 "為另一個 Kotlin 類別衍生外部序列化器(實驗性)"。 object DateAsLongSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } 我們無法通過  @Serializable 註解將  DateAsLongSerializer 序列化器綁定到  Date 類別,因為我們無法控制  Date 的源代碼。這裡有幾種解決方法。 手動傳遞序列化器 所有的  encodeToXxx 和  decodeFromXxx 函數都有一個帶有第一個序列化器參數的重載版本。當一個不可序列化的類別(如  Date)是正在序列化的頂級類別時,我們可以使用這些重載版本。 fun main() { val kotlin10ReleaseDate = SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00") println(Json.encodeToString(DateAsLongSerializer, kotlin10ReleaseDate)) } 輸出: 1455494400000 為屬性指定序列化器 當不可序列化類別(如  Date)的屬性作為可序列化類別的一部分被序列化時,我們必須為其提供序列化器,否則代碼將無法編譯。這可以通過在屬性上使用  @Serializable 註解來實現。 @Serializable class ProgrammingLanguage( val name: String, @Serializable(with = DateAsLongSerializer::class) val stableReleaseDate: Date ) fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(Json.encodeToString(data)) } stableReleaseDate 屬性將使用我們為其指定的序列化策略進行序列化: {"name":"Kotlin","stableReleaseDate":1455494400000} 為特定類型指定序列化器 @Serializable 註解也可以直接應用於類型。當需要為像  Date 這樣的類型提供自定義序列化器時,這非常方便。最常見的用例是當你有一個日期列表時: @Serializable class ProgrammingLanguage( val name: String, val releaseDates: List<@Serializable(DateAsLongSerializer::class) Date> ) fun main() { val df = SimpleDateFormat("yyyy-MM-ddX") val data = ProgrammingLanguage("Kotlin", listOf(df.parse("2023-07-06+00"), df.parse("2023-04-25+00"), df.parse("2022-12-28+00"))) println(Json.encodeToString(data)) } 輸出: {"name":"Kotlin","releaseDates":[1688601600000,1682380800000,1672185600000]} 為檔案指定序列化器 可以通過在檔案開頭使用檔案級別的  UseSerializers 註解來為整個檔案中的特定類型(如  Date)指定序列化器。 @file:UseSerializers(DateAsLongSerializer::class) 現在可以在可序列化類別中使用  Date 屬性,而不需要額外的註解。 @Serializable class ProgrammingLanguage(val name: String, val stableReleaseDate: Date) fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(Json.encodeToString(data)) } 輸出: {"name":"Kotlin","stableReleaseDate":1455494400000} 使用型別別名全局指定序列化器 在處理序列化策略時,kotlinx.serialization 傾向於成為始終顯式的框架:通常,應在  @Serializable 註解中明確提到它們。因此,我們不提供任何類型的全局序列化器配置(除非後面提到的上下文序列化器)。 然而,在有大量檔案和類別的專案中,每次指定  @file:UseSerializers 可能太過繁瑣,尤其是對於像  Date 或  Instant 這樣在專案中有固定序列化策略的類別。對於這些情況,可以使用型別別名指定序列化器,因為它們會保留註解,包括與序列化相關的註解: typealias DateAsLong = @Serializable(DateAsLongSerializer::class) Date typealias DateAsText = @Serializable(DateAsSimpleTextSerializer::class) Date 使用這些新的不同類型,可以在沒有額外註解的情況下序列化  Date: @Serializable class ProgrammingLanguage(val stableReleaseDate: DateAsText, val lastReleaseTimestamp: DateAsLong) fun main() { val format = SimpleDateFormat("yyyy-MM-ddX") val data = ProgrammingLanguage(format.parse("2016-02-15+00"), format.parse("2022-07-07+00")) println(Json.encodeToString(data)) } 輸出: {"stableReleaseDate":"2016-02-15","lastReleaseTimestamp":1657152000000} 泛型類型的自定義序列化器 讓我們看一下泛型  Box 類別的例子。我們計劃為其編寫一個自定義序列化策略,因此將其標記為  @Serializable(with = BoxSerializer::class)。 @Serializable(with = BoxSerializer::class) data class Box(val contents: T) 為常規類型編寫  KSerializer 的實現就像我們在本章中的  Color 類型例子中所見那樣,作為一個  object 來處理。而對於泛型類別的序列化器,它需要一個構造函數,該構造函數接受與類型具有的泛型參數數量相同的  KSerializer 參數。讓我們編寫一個  Box 序列化器,在序列化過程中擦除自身,並將所有工作委派給其  data 屬性的底層序列化器。 class BoxSerializer(private val dataSerializer: KSerializer) : KSerializer> { override val descriptor: SerialDescriptor = dataSerializer.descriptor override fun serialize(encoder: Encoder, value: Box) = dataSerializer.serialize(encoder, value.contents) override fun deserialize(decoder: Decoder) = Box(dataSerializer.deserialize(decoder)) } 現在我們可以序列化和反序列化  Box。 @Serializable data class Project(val name: String) fun main() { val box = Box(Project("kotlinx.serialization")) val string = Json.encodeToString(box) println(string) println(Json.decodeFromString>(string)) } 生成的 JSON 看起來像是直接序列化了  Project 類別。 {"name":"kotlinx.serialization"} Box(contents=Project(name=kotlinx.serialization)) 特定格式的序列化器 上述自定義序列化器對每種格式的工作方式相同。然而,可能存在格式特定的功能,序列化器實現可能希望利用這些功能。 "JSON 轉換" 章節提供了利用 JSON 特定功能的序列化器範例。 格式實現可以有針對某種類型的格式特定表示,如 "替代和自定義格式(實驗性)" 章節中的 "格式特定類型" 所解釋的。 本章接下來將介紹根據上下文調整序列化策略的通用方法。 上下文序列化 之前所有的自定義序列化策略都是靜態的,即在編譯時完全定義。例外情況是 "手動傳遞序列化器" 方法,但它僅適用於頂級物件。你可能需要在執行時更改深層物件樹中的物件的序列化策略,策略的選擇是基於上下文的。例如,你可能希望根據序列化資料的協議版本在 JSON 格式中將  java.util.Date 表示為 ISO 8601 字串或長整數。這就是所謂的上下文序列化,並且它由內建的  ContextualSerializer 類別支持。通常我們不需要顯式使用這個序列化器類別——可以使用  @Contextual 註解作為  @Serializable(with = ContextualSerializer::class) 註解的快捷方式,或者可以像  UseSerializers 註解那樣在檔案級別使用  UseContextualSerialization 註解。讓我們看一個利用前者的範例。 @Serializable class ProgrammingLanguage( val name: String, @Contextual val stableReleaseDate: Date ) 要實際序列化這個類別,我們必須在調用  encodeToXxx/decodeFromXxx 函數時提供對應的上下文。否則,我們將得到 "Serializer for class 'Date' is not found" 的異常。 序列化器模組 要提供上下文,我們需要定義一個  SerializersModule 實例,該實例描述了在執行時應該使用哪些序列化器來序列化哪些上下文可序列化類別。這可以使用  SerializersModule {} 構建函數完成,該函數提供了  SerializersModuleBuilder DSL 來註冊序列化器。在下面的範例中,我們使用了帶有序列化器的  contextual 函數。對應的類別將通過內聯類型參數自動獲取該序列化器。 private val module = SerializersModule { contextual(DateAsLongSerializer) } 接下來,我們使用  Json {} 構建函數和  serializersModule 屬性創建一個帶有這個模組的 JSON 格式實例。 關於自定義 JSON 配置的詳細信息可以在 "JSON 配置" 章節中找到。 val format = Json { serializersModule = module } 現在我們可以使用這個格式序列化我們的資料。 fun main() { val data = ProgrammingLanguage("Kotlin", SimpleDateFormat("yyyy-MM-ddX").parse("2016-02-15+00")) println(format.encodeToString(data)) } 輸出: {"name":"Kotlin","stableReleaseDate":1455494400000} 上下文序列化與泛型類別 在上一節中,我們看到可以在模組中註冊序列化器實例,以便上下文中序列化我們想要的類別。我們還知道泛型類別的序列化器具有構造函數參數——類型參數序列化器。這意味著我們不能為類別使用一個序列化器實例,如果這個類別是泛型的: val incorrectModule = SerializersModule { // 只能序列化 Box,而不能序列化 Box 或其他類型 contextual(BoxSerializer(Int.serializer())) } 當我們想要上下文序列化泛型類別時,可以在模組中註冊提供者: val correctModule = SerializersModule { // args[0] 包含 Int.serializer() 或 String.serializer(),具體取決於使用情況 contextual(Box::class) { args -> BoxSerializer(args[0]) } } 關於序列化模組的額外細節可以在 "多型性" 章節的 "合併庫序列化模組" 部分中找到。 為其他 Kotlin 類別衍生外部序列化器(實驗性) 如果要序列化的第三方類別是具有僅包含屬性的主要構造函數的 Kotlin 類別(這種類別本可以標記為  @Serializable),那麼您可以使用  Serializer 註解並在物件上設置  forClass 屬性來為其生成一個外部序列化器。 // NOT @Serializable class Project(val name: String, val language: String) @Serializer(forClass = Project::class) object ProjectSerializer 您必須使用本章中解釋的一種方法將此序列化器綁定到類別。我們將按照 "手動傳遞序列化器" 方法進行此示例。 fun main() { val data = Project("kotlinx.serialization", "Kotlin") println(Json.encodeToString(ProjectSerializer, data)) } 這將所有的  Project 屬性序列化: {"name":"kotlinx.serialization","language":"Kotlin"} 外部序列化使用屬性 正如我們之前看到的,常規  @Serializable 註解會創建一個序列化器,以便序列化備援欄位。使用  Serializer(forClass = ...) 的外部序列化無法訪問備援欄位,因此其工作方式不同。它僅 序列化具有 setter 的可訪問屬性或屬於主要構造函數的屬性。以下範例展示了這一點。 // NOT @Serializable, will use external serializer class Project( // val in a primary constructor -- serialized val name: String ) { var stars: Int = 0 // property with getter & setter -- serialized val path: String // getter only -- not serialized get() = "kotlin/$name" private var locked: Boolean = false // private, not accessible -- not serialized } @Serializer(forClass = Project::class) object ProjectSerializer fun main() { val data = Project("kotlinx.serialization").apply { stars = 9000 } println(Json.encodeToString(ProjectSerializer, data)) } 輸出如下: {"name":"kotlinx.serialization","stars":9000} 下一章將介紹多型性。