一. Codable的简单使用
Codable
是 Encodable
和 Decodable
协议的聚合。 同时拥有序列化和反序列化的能力。
swift
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
public protocol Decodable {
init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
使用起来很简单:
swift
struct Feed: Codable {
var name: String
var id: Int
}
let dict = [
"id": 2,
"name": "小明",
] as [String : Any]
let jsonStr = dict.bt_toJSONString() ?? ""
guard let jsonData = jsonStr.data(using: .utf8) else { return }
let decoder = JSONDecoder()
do {
let feed = try decoder.decode(Feed.self, from: jsonData)
print(feed)
} catch let error {
print(error)
}
// Feed(name: "小明", id: 2)
开始使用codeable,感觉一切都很美好。带着这份美好,开始学习Codable。
为了介绍Codable协议,写了挺多演示代码,为了减少示例中的代码量,封装了编码和解码的方法,演示代码将直接使用这四个方法。
swift
extension Dictionary {
/// 解码
public func decode<T: Decodable>(type: T.Type) -> T? {
do {
guard let jsonStr = self.toJSONString() else { return nil }
guard let jsonData = jsonStr.data(using: .utf8) else { return nil }
let decoder = JSONDecoder()
let obj = try decoder.decode(type, from: jsonData)
return obj
} catch let error {
print(error)
return nil
}
}
/// 字典转json字符串
private func toJSONString() -> String? {
if (!JSONSerialization.isValidJSONObject(self)) {
print("无法解析出JSONString")
return nil
}
do {
let data = try JSONSerialization.data(withJSONObject: self, options: [])
let json = String(data: data, encoding: String.Encoding.utf8)
return json
} catch {
print(error)
return nil
}
}
}
extension String {
/// 解码
public func decode<T: Decodable>(type: T.Type) -> T? {
guard let jsonData = self.data(using: .utf8) else { return nil }
do {
let decoder = JSONDecoder()
let feed = try decoder.decode(type, from: jsonData)
return feed
} catch let error {
print(error)
return nil
}
}
}
extension Encodable {
/// 编码
public func encode() -> Any? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let data = try encoder.encode(self)
guard let value = String(data: data, encoding: .utf8) else { return nil }
return value
} catch {
print(error)
return nil
}
}
}
二. Decodable
Decodable 是一个协议,提供的 init 方法中包含 Decoder 协议。
swift
public protocol Decodable {
init(from decoder: Decoder) throws
}
swift
public protocol Decoder {
// 在解码过程中达到这一点所采用的编码路径.
var codingPath: [CodingKey] { get }
// 设置的用于解码的任何上下文信息.
var userInfo: [CodingUserInfoKey : Any] { get }
// 生成有键解码容器
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
// 生成无键解码容器
func unkeyedContainer() throws -> UnkeyedDecodingContainer
// 生成单解码容器
func singleValueContainer() throws -> SingleValueDecodingContainer
}
1. DecodingError
解码失败,抛出异常DecodingError。
swift
public enum DecodingError : Error {
/// 出现错误时的上下文
public struct Context : Sendable {
/// 到达解码调用失败点所采取的编码密钥路径.
public let codingPath: [CodingKey]
/// 错误的描述信息
public let debugDescription: String
public let underlyingError: Error?
public init(codingPath: [CodingKey], debugDescription: String, underlyingError: Error? = nil)
}
// 表示类型不匹配的错误。当解码器期望将JSON值解码为特定类型,但实际值的类型与期望的类型不匹配时,会引发此错误。
case typeMismatch(Any.Type, DecodingError.Context)
// 表示找不到值的错误。当解码器期望从JSON中提取某个值,但该值不存在时,会引发此错误。
case valueNotFound(Any.Type, DecodingError.Context)
// 表示找不到键的错误。当解码器期望在JSON中找到某个键,但在给定的数据中找不到该键时,会引发此错误。
case keyNotFound(CodingKey, DecodingError.Context)
// 表示数据损坏的错误。当解码器无法从给定的数据中提取所需的值时,会引发此错误。比如解析一个枚举值的时候。
case dataCorrupted(DecodingError.Context)
}
错误中包含许多有用信息:
- 解码的key
- 解码时候的上下文信息
- 解码的类型
这些信息,将为我们的解码失败的兼容提供重要帮助,在下一篇章关于SmartCodable实现中体现价值。
2. 三种解码容器
- KeyedDecodingContainer: 用于解码包含键值对的JSON对象,例如字典。它提供了一种访问和解码特定键的值的方式。
- UnkeyedDecodingContainer: 用于解码无键的JSON数组。例如数组,它提供了一种访问下标解码值的方式。
- SingleValueDecodingContainer:用于解码单个值的JSON数据,例如字符串、布尔值。它提供了一种访问和解码单个值的方式。
SingleValueDecodingContainer
在Swift中,SingleValueDecodingContainer是用于解码单个值的协议。它提供了一些方法来解码不同类型的值,例如字符串、整数、浮点数等。
SingleValueDecodingContainer没有设计decodeIfPresent方法的原因是,它的主要目的是解码单个值,而不是处理可选值。它假设解码的值始终存在,并且如果解码失败,会抛出一个错误。
swift
public protocol SingleValueDecodingContainer {
var codingPath: [CodingKey] { get }
/// 解码空值时返回true
func decodeNil() -> Bool
/// 解码给定类型的单个值。
func decode<T>(_ type: T.Type) throws -> T where T : Decodable
}
使用起来也很简单。如果知道元素的类型,可以使用decode(_:)
方法直接解码。如果不知道元素的类型,可以使用decodeNil()
方法检查元素是否为nil
。
python
struct Feed: Decodable {
let string: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
string = try container.decode(String.self)
}
}
let json = """
"Hello, World!"
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(string: "Hello, World!")
UnkeyedDecodingContainer
UnkeyedDecodingContainer
是Swift中的一个协议,用于解码无键的容器类型数据,可以按顺序访问和解码容器中的元素,例如数组。可以使用 count
属性获取容器中的元素数量,并使用 isAtEnd
属性检查是否已经遍历完所有元素。
swift
public protocol UnkeyedDecodingContainer {
var codingPath: [CodingKey] { get }
/// 容器中的元素数量
var count: Int? { get }
/// 检查是否已经遍历完所有元素
var isAtEnd: Bool { get }
/// 容器的当前解码索引(即下一个要解码的元素的索引)。每次解码调用成功后递增。
var currentIndex: Int { get }
/// 解码null值的时候,返回true(前提是必须有这个键)
mutating func decodeNil() throws -> Bool
/// 解码指定类型的值
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable
mutating func decode(_ type: Bool.Type) throws -> Bool
mutating func decode(_ type: String.Type) throws -> String
mutating func decode(_ type: Double.Type) throws -> Double
......
/// 解码指定类型的值(可选值)
mutating func decodeIfPresent<T>(_ type: T.Type) throws -> T? where T : Decodable
mutating func decodeIfPresent(_ type: Bool.Type) throws -> Bool?
mutating func decodeIfPresent(_ type: String.Type) throws -> String?
mutating func decodeIfPresent(_ type: Double.Type) throws -> Double?
......
/// 解码嵌套的容器类型,并返回一个新的KeyedDecodingContainer
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey
/// 解码嵌套的无键容器类型,并返回一个新的UnkeyedDecodingContainer。
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer
/// 获取父类的容器。
mutating func superDecoder() throws -> Decoder
}
如果知道元素的类型,可以使用decode(_:forKey:)
方法直接解码。可以使用decodeNil()
方法检查元素是否为nil
。
ini
struct Feed: Codable {
var value1: Int = 0
var value2: Int = 0
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
if try container.decodeNil() {
value1 = 0
} else {
value1 = try container.decode(Int.self)
}
if try container.decodeNil() {
value2 = 0
} else {
value2 = try container.decode(Int.self)
}
}
}
let json = """
[1, 2]
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(value1: 1, value2: 2)
这个数组数据[1, 2], 按照index的顺序逐个解码。
数据 [1,2] 和模型属性 [value1,value2] 按照顺序一一对应。就形成对应关系 [1 -> value1] , [2 -> value2] 。
如果继续解码将报错:
swift
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
if try container.decodeNil() {
value1 = 0
} else {
value1 = try container.decode(Int.self)
}
if try container.decodeNil() {
value2 = 0
} else {
value2 = try container.decode(Int.self)
}
do {
try container.decodeNil()
} catch {
print(error)
}
}
// 报错信息
▿ DecodingError
▿ valueNotFound : 2 elements
- .0 : Swift.Optional<Any>
▿ .1 : Context
▿ codingPath : 1 element
▿ 0 : _JSONKey(stringValue: "Index 2", intValue: 2)
- stringValue : "Index 2"
▿ intValue : Optional<Int>
- some : 2
- debugDescription : "Unkeyed container is at end."
- underlyingError : nil
KeyedDecodingContainer
KeyedDecodingContainer
用于解码键值对类型的数据。它提供了一种通过键从容器中提取值的方式。
swift
public struct KeyedDecodingContainer<K> : KeyedDecodingContainerProtocol where K : CodingKey {
public typealias Key = K
public init<Container>(_ container: Container) where K == Container.Key, Container : KeyedDecodingContainerProtocol
/// 在解码过程中达到这一点所采用的编码密钥路径。
public var codingPath: [CodingKey] { get }
/// 解码器对这个容器的所有密钥。
public var allKeys: [KeyedDecodingContainer<K>.Key] { get }
/// 返回一个布尔值,该值指示解码器是否包含与给定键关联的值。
public func contains(_ key: KeyedDecodingContainer<K>.Key) -> Bool
/// 解码null数据时,返回true
public func decodeNil(forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool
/// 为给定键解码给定类型的值。
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable
public func decode(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool
public func decode(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String
public func decode(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double
......
/// 为给定键解码给定类型的值(如果存在)。如果容器没有值,该方法返回' nil '
public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? where T : Decodable
public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool?
public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?
public func decodeIfPresent(_ type: Double.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Double?
......
/// 允许您在解码过程中创建一个新的嵌套容器,解码一个包含嵌套字典的JSON对象,以便解码更复杂的数据结构。
public func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey
/// 允许您在解码过程中创建一个新的嵌套容器,解码一个包含嵌套数组的JSON对象,以便解码更复杂的数据结构。
public func nestedUnkeyedContainer(forKey key: KeyedDecodingContainer<K>.Key) throws -> UnkeyedDecodingContainer
/// 返回一个' Decoder '实例用于从容器中解码' super '。
/// 相当于calling `superDecoder(forKey:)` with `Key(stringValue: "super", intValue: 0)`.
public func superDecoder() throws -> Decoder
}
解码动态数据
出行工具的选择有三种: walk,riding,publicTransport。一次出行只会选择其中的一种,即服务端只会下发一种数据。
步行出行:
json
{
"travelTool": "walk",
"walk": {
"hours": 3,
}
}
骑行出行:
json
{
"travelTool": "riding",
"riding": {
"hours":2,
"attention": "注意带头盔"
}
}
公共交通出行:
json
{
"travelTool": "publicTransport",
"publicTransport": {
"hours": 1,
"transferTimes": "3",
}
}
通过重写init(from decoder: Decoder) 方法,使用decodeIfPresent处理动态键值结构。
php
struct Feed: Decodable {
var travelTool: Tool
var walk: Walk?
var riding: Riding?
var publicTransport: PublicTransport?
enum CodingKeys: CodingKey {
case travelTool
case walk
case riding
case publicTransport
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
travelTool = try container.decode(Tool.self, forKey: .travelTool)
walk = try container.decodeIfPresent(Walk.self, forKey: .walk)
riding = try container.decodeIfPresent(Riding.self, forKey: .riding)
publicTransport = try container.decodeIfPresent(PublicTransport.self, forKey: .publicTransport)
}
enum Tool: String, Codable {
case walk
case riding
case publicTransport
}
struct Walk: Codable {
var hours: Int
}
struct Riding: Codable {
var hours: Int
var attention: String
}
struct PublicTransport: Codable {
var hours: Int
var transferTimes: String
}
}
let json = """
{
"travelTool": "publicTransport",
"publicTransport": {
"hours": 1,
"transferTimes": "3",
}
}
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(travelTool: Feed.Tool.publicTransport, walk: nil, riding: nil, publicTransport: Optional(Feed.PublicTransport(hours: 1, transferTimes: "3")))
3. keyDecodingStrategy
keyDecodingStrategy 是Swift4.1之后提供的decode策略,用于在解码之前自动更改密钥值的策略。它一个枚举值,提供了三种策略:
less
public enum KeyDecodingStrategy : Sendable {
/// 使用每种类型指定的键,默认策略。
case useDefaultKeys
/// 使用小驼峰策略
case convertFromSnakeCase
/// 使用自定义策略
@preconcurrency case custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey)
}
使用起来也比较简单
swift
do {
let decoder = JSONDecoder()
/// 默认的
// decoder.keyDecodingStrategy = .useDefaultKeys
/// 小驼峰 read_name -> readName
// decoder.keyDecodingStrategy = .convertFromSnakeCase
/// 自定义 需要返回CodingKey类型。
decoder.keyDecodingStrategy = .custom({ codingPath in
for path in codingPath {
if path.stringValue == "read_name" {
return CustomJSONKey.init(stringValue: "readName")!
} else {
return path
}
}
return CustomJSONKey.super
})
let feed = try decoder.decode(DecodingStrtegyFeed.self, from: jsonData)
print(feed)
} catch let error {
print(error)
}
4. 解码异常情况
进行 decode 时候,遇到以下三种情况就会失败。并且只有一个属性解析失败时就抛出异常,导致整个解析失败。
- 类型键不存在
- 类型键不匹配
- 数据值是null
演示样例
css
struct Feed: Codable {
var hobby: Hobby
}
struct Hobby: Codable {
var name: String
var year: Int
}
正常的json数据返回是这样的:
python
let json = """
{
"hobby": {
"name": "basketball",
"year": 3
}
}
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
解码正常,输出为: Feed(hobby: Hobby(name: "basketball", year: 3))
我们将基于这个数据模型,对以下四种特殊数据场景寻找解决方案。
1. 类型键不存在
ini
let json = """
{
"hobby": {}
}
"""
数据返回了空对象。
less
▿ DecodingError
▿ keyNotFound : 2 elements
- .0 : CodingKeys(stringValue: "name", intValue: nil)
▿ .1 : Context
▿ codingPath : 1 element
- 0 : CodingKeys(stringValue: "hobby", intValue: nil)
- debugDescription : "No value associated with key CodingKeys(stringValue: "name", intValue: nil) ("name")."
- underlyingError : nil
解码失败的原因是,Hobby的属性解析失败。缺少Hobby的name属性对应的值。
兼容方案1
csharp
struct Hobby: Codable {
var name: String?
var year: Int?
}
将Hobby的属性设置为可选类型,会被自动解析为nil。会输出:Feed(hobby: CodableTest.Hobby(name: nil, year: nil))
兼容方案2
swift
struct Feed: Codable {
var hobby: Hobby?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.hobby = try container.decode(Hobby.self, forKey: .hobby)
} catch {
self.hobby = nil
}
}
}
将Hobby设置为可选类型,使用 do-catch。
2. 类型键不匹配
ini
let json = """
{
"hobby": "basketball"
}
"""
数据返回了类型不匹配的值。
swift
▿ DecodingError
▿ typeMismatch : 2 elements
- .0 : Swift.Dictionary<Swift.String, Any>
▿ .1 : Context
▿ codingPath : 1 element
- 0 : CodingKeys(stringValue: "hobby", intValue: nil)
- debugDescription : "Expected to decode Dictionary<String, Any> but found a string/data instead."
- underlyingError : nil
兼容方案:
swift
struct Feed: Codable {
var hobby: Hobby?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.hobby = try container.decode(Hobby.self, forKey: .hobby)
} catch {
self.hobby = nil
}
}
}
3. 数据值为null
ini
let json = """
{
"hobby": null
}
"""
数据返回了null值。
yaml
▿ DecodingError
▿ valueNotFound : 2 elements
- .0 : Swift.Int
▿ .1 : Context
▿ codingPath : 1 element
- 0 : CodingKeys(stringValue: "id", intValue: nil)
- debugDescription : "Expected Int value but found null instead."
- underlyingError : nil
兼容方案1:
css
struct Feed: Codable {
var hobby: Hobby?
}
将hobby设置为可选值,Codable会自动将null映射为nil,很容易就解决了这个问题。
兼容方案2:
swift
struct Feed: Codable {
var hobby: Hobby?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if try container.decodeNil(forKey: .hobby) {
self.hobby = nil
} else {
self.hobby = try container.decode(Hobby.self, forKey: .hobby)
}
}
}
判断是否解析为nil。其实就是方案1的逻辑。
这种兼容在商用业务中可以被接受么?
类型键不存在 和 数据值是null 虽然可以通过可选类型避免,但是类型不匹配的情况,只能重写协议来避免。你可以想象一下这种痛苦以及不确定性。
1. 可选绑定的弊端
使用可选势必会有大量的可选绑定,对于 enum 和 Bool 的可选的使用是非常痛苦的。
2. 重写协议方法
重写协议方法,会增加代码量,并且很容易产生错误(如何保证兼容的全面性?),导致解析失败。
一个属性的完整兼容应该包含以下三个步骤,可以想象如果一个模型有上百个字段的场景(对于我们这样做数据业务的app非常常见)。
- 该字段是否存在
container.contains(.name)
- 数据是否为null
try container.decodeNil(forKey: .name)
- 是否类型匹配
try container.decode(String.self, forKey: .name)
swift
struct Person: Codable {
var name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.name) { // 数据中是否否包含该键
if try container.decodeNil(forKey: .name) { // 是否为nil
self.name = ""
} else {
do {
// 是否类型正确
self.name = try container.decode(String.self, forKey: .name)
} catch {
self.name = ""
}
}
} else {
self.name = ""
}
}
}
三. Encodable
swift
public protocol Encodable {
func encode(to encoder: Encoder) throws
}
swift
public protocol Encoder {
var codingPath: [CodingKey] { get }
var userInfo: [CodingUserInfoKey : Any] { get }
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey
func unkeyedContainer() -> UnkeyedEncodingContainer
func singleValueContainer() -> SingleValueEncodingContainer
}
Encodable协议相对Decodable协议较为简单,请参考Decodable篇。
1. EncodingError
编码失败,就会抛出异常,此时的error就是EncodingError。
java
public enum EncodingError : Error {
case invalidValue(Any, EncodingError.Context)
}
此Error就只有一种情况: 表示要编码的值无效或不符合预期的格式要求。
2. 三种编码容器
关于三种编码容器SingleValueDecodingContainer , UnkeyedDecodingContainer , KeyedDecodingContainer , 请参考 Decodable 的介绍。
3.编码时的 Optional值
Person模型中的name属性是可选值,并设置为了nil。进行encode的时候,不会以 null 写入json中。
swift
struct Person: Encodable {
let name: String?
init() {
name = nil
}
}
let encoder = JSONEncoder()
guard let jsonData = try? encoder.encode(Person()) else { return }
guard let json = String(data: jsonData, encoding: .utf8) else { return }
print(json)
// 输出: {}
这是因为系统进行encode的时候,使用的是 encodeIfPresent, 该方法不会对nil进行encode。等同于:
swift
struct Person: Encodable {
let name: String?
init() {
name = nil
}
enum CodingKeys: CodingKey {
case name
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(self.name, forKey: .name)
// try container.encode(name, forKey: .name)
}
}
如果需要将这种情况仍然要写入json中,可以使用 try container.encode(name, forKey: .name) 。输出信息为: {"name":null}
4. encode 和 encodeIfPresent
如果不重写 func encode(to encoder: Encoder) throws
系统会根据encode属性是否可选类型决定使用哪个方法。
- 可选属性:默认使用encodeIfPresent方法
- 非可选属性:默认使用encode方法
四. userInfo
userInfo是一个 [CodingUserInfoKey : Any] 类型的字典,用于存储与解码过程相关的任意附加信息。它可以用来传递自定义的上下文数据或配置选项给解码器。可以在初始化Decoder时设置userInfo属性,并在解码过程中使用它来访问所需的信息。这对于在解码过程中需要使用额外的数据或配置选项时非常有用。
以下是一些使用userInfo属性的示例场景:
- 自定义解码行为:可以使用userInfo属性传递自定义的解码选项或配置给Decoder。例如,您可以在userInfo中设置一个布尔值,以指示解码器在遇到特定的键时执行特殊的解码逻辑。
- 传递上下文信息:如果需要在解码过程中访问一些上下文信息,例如用户身份验证令牌或当前语言环境设置。
- 错误处理:可以在userInfo中存储有关错误处理的信息。例如可以将一个自定义的错误处理器存储在userInfo中,以便在解码过程中捕获和处理特定类型的错误。
swift
struct Feed: Codable {
let name: String
let age: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
// 从userInfo中获取上下文信息
if let language = decoder.userInfo[.init(rawValue: "language")!] as? String,
let version = decoder.userInfo[.init(rawValue: "version")!] as? Double {
print("Decoded using (language) (version)")
// Decoded using Swift 5.0
}
}
}
let jsonData = """
{
"name": "John Doe",
"age": 30
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
// 设置userInfo属性,传递自定义的上下文信息
let userInfo: [CodingUserInfoKey: Any] = [
.init(rawValue: "language")!: "Swift",
.init(rawValue: "version")!: 5.0]
decoder.userInfo = userInfo
guard let feed = try? decoder.decode(Feed.self, from: jsonData) else { return }
print(feed)
五. CodingKey
在Swift中,CodingKey
是一个协议,用于在编码和解码过程中映射属性的名称。它允许 自定义属性名称 与 编码键或解码键 之间的映射关系。CodingKey是Codable协议的核心之一,尤其是处理复杂结构的数据,发挥着至关重要的作用。
当使用 Codable
来编码和解码自定义类型时,Swift会自动合成编码和解码的实现。
- 对于编码,Swift会将类型的属性名称作为键,将属性的值编码为相应的数据格式。
- 对于解码,Swift会使用键来匹配编码的数据,并将数据解码为相应的属性值。
swift
public protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible, Sendable {
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
init?(intValue: Int)
}
CodingKey
协议要求实现一个名为 stringValue
的属性,用于表示键的字符串值。此外,还可以选择实现一个名为 intValue
的属性,用于表示键的整数值(用于处理有序容器,如数组)。
通过实现 CodingKey
协议,可以在编码和解码过程中使用自定义的键,以便更好地控制属性和编码键之间的映射关系。这对于处理不匹配的属性名称或与外部数据源进行交互时特别有用。
1. 处理不匹配的属性名称
有时候属性的名称和编码的键不完全匹配,或者希望使用不同的键来编码和解码属性。这时,可以通过实现 CodingKey
协议来自定义键。
使用CodingKey将 nick_name 字段重命名为 name。
python
struct Feed: Codable {
var name: String
var id: Int = 0
private enum CodingKeys: String, CodingKey {
case name = "nick_name"
// 只实现声明的属性的映射。
// case id
}
}
let json = """
{
"nick_name": "xiaoming",
"id": 10
}
"""
guard let feed = json.decode(type: Feed.self) else { return }
print(feed)
// Feed(name: "xiaoming", id: 0)
通过实现CodingKey协议,我们成功的将不匹配的属性名称(nick_name) 完成了映射。
- CodingKeys只会处理实现的映射关系,没实现的映射关系(比如: id),将不会解析。
- CodingKeys最好使用private修饰,避免被派生类继承。
- CodingKeys必须是嵌套在声明的struct中的。
2. 扁平化解析
我们可以用枚举来实现CodingKey,用来处理不匹配的属性映射关系。还可以使用结构体来实现,提供更灵活的使用,处理复杂的层级结构。
bash
let res = """
{
"player_name": "balabala Team",
"age": 20,
"native_Place": "shandong",
"scoreInfo": {
"gross_score": 2.4,
"scores": [
0.9,
0.8,
0.7
],
"remarks": {
"judgeOne": {
"content": "good"
},
"judgeTwo": {
"content": "very good"
},
"judgeThree": {
"content": "bad"
}
}
}
}
""
提供这样一个较为复杂的json结构,期望将这个json扁平化的解析到Player结构体中。
vbnet
struct Player {
let name: String
let age: Int
let nativePlace: String
let grossScore: CGFloat
let scores: [CGFloat]
let remarks: [Remark]
}
struct Remark {
var judge: String
var content: String
}
/** 解析完成的结构是这样的
Player(
name: "balabala Team",
age: 20,
nativePlace: "shandong",
grossScore: 2.4,
scores: [0.9, 0.8, 0.7],
remarks: [
Remark(judge: "judgeTwo", content: "very good"),
Remark(judge: "judgeOne", content: "good"),
Remark(judge: "judgeThree", content: "bad")
]
)
*/
实现逻辑是这样的:
swift
struct Remark: Codable {
var judge: String
var content: String
}
struct Player: Codable {
let name: String
let age: Int
let nativePlace: String
let grossScore: CGFloat
let scores: [CGFloat]
let remarks: [Remark]
// 当前容器中需要包含的key
enum CodingKeys: String, CodingKey {
case name = "player_name"
case age
case nativePlace = "native_Place"
case scoreInfo
}
enum ScoreInfoCodingKeys: String, CodingKey {
case grossScore = "gross_score"
case scores
case remarks
}
struct RemarkCodingKeys: CodingKey {
var intValue: Int? {return nil}
init?(intValue: Int) {return nil}
var stringValue: String //json中的key
init?(stringValue: String) {
self.stringValue = stringValue
}
static let content = RemarkCodingKeys(stringValue: "content")!
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.nativePlace = try container.decode(String.self, forKey: .nativePlace)
let scoresContainer = try container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
self.grossScore = try scoresContainer.decode(CGFloat.self, forKey: .grossScore)
self.scores = try scoresContainer.decode([CGFloat].self, forKey: .scores)
let remarksContainer = try scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
var remarks: [Remark] = []
for key in remarksContainer.allKeys { //key的类型就是映射规则的类型(Codingkeys)
let judge = key.stringValue
print(key)
/**
RemarkCodingKeys(stringValue: "judgeTwo", intValue: nil)
RemarkCodingKeys(stringValue: "judgeOne", intValue: nil)
RemarkCodingKeys(stringValue: "judgeThree", intValue: nil)
*/
let keyedContainer = try remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: key)
let content = try keyedContainer.decode(String.self, forKey: .content)
let remark = Remark(judge: judge, content: content)
remarks.append(remark)
}
self.remarks = remarks
}
func encode(to encoder: Encoder) throws {
// 1. 生成最外层的字典容器
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(nativePlace, forKey: .nativePlace)
// 2. 生成scoreInfo字典容器
var scoresContainer = container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
try scoresContainer.encode(grossScore, forKey: .grossScore)
try scoresContainer.encode(scores, forKey: .scores)
// 3. 生成remarks字典容器(模型中结构是数组,但是我们要生成字典结构)
var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
for remark in remarks {
var remarkContainer = remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: RemarkCodingKeys(stringValue: remark.judge)!)
try remarkContainer.encode(remark.content, forKey: .content)
}
}
}
1. 解码最外层数据
层级结构:包含4个key的字典结构。
json
{
"player_name": ...
"age": ...
"native_Place": ...
"scoreInfo": ...
}
-
生成容器
根据这个结构,使用这个CodingKeys实现数据和模型之间的映射关系。
phpenum CodingKeys: String, CodingKey { case name = "player_name" case age case nativePlace = "native_Place" case scoreInfo } var container = encoder.container(keyedBy: CodingKeys.self)
-
容器解码
phptry container.encode(name, forKey: .name) try container.encode(age, forKey: .age) try container.encode(nativePlace, forKey: .nativePlace) // scoreInfo是下一层的数据
2. 解码scoreInfo层数据
层级结构:包含3个key的字典结构。
json
{
"gross_score": ...
"scores": ...
"remarks": ...
}
-
生成容器
根据这个结构,我们使用这个ScoreInfoCodingKeys实现本层的数据和模型之间的映射关系。
phpenum ScoreInfoCodingKeys: String, CodingKey { case grossScore = "gross_score" case scores case remarks } var scoresContainer = container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
-
容器解码
phptry scoresContainer.encode(grossScore, forKey: .grossScore) try scoresContainer.encode(scores, forKey: .scores) // remarks层是下层的数据
3. 解码remarks层数据
我们期望进行这样的层级转换
-
生成容器
swiftstruct RemarkCodingKeys: CodingKey { var intValue: Int? {return nil} init?(intValue: Int) {return nil} var stringValue: String //json中的key init?(stringValue: String) { self.stringValue = stringValue } static let content = RemarkCodingKeys(stringValue: "content")! } var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
使用scoresContainer容器生成一个新的remarksContainer容器,表示remarksContainer容器是在scoresContainer容器内。
-
容器解码
phpfor remark in remarks { var remarkContainer = remarksContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: RemarkCodingKeys(stringValue: remark.judge)!) try remarkContainer.encode(remark.content, forKey: .content) }
remarks即模型中的[Remark]类型的属性。遍历remarks,使用remarksContainer生成一个新的容器remarkContainer。
该容器使用RemarkCodingKeys作为映射关系,使用remark的judge生成的CodingKey作为Key。 即:
lessRemarkCodingKeys(stringValue: "judgeTwo", intValue: nil) RemarkCodingKeys(stringValue: "judgeOne", intValue: nil) RemarkCodingKeys(stringValue: "judgeThree", intValue: nil)
最后将数据填充到remarkContainer里面:
try remarkContainer.encode(remark.content, forKey: .content)
。
4. 总结
-
CodingKey即可以是枚举,又可以是结构体。
-
通过CodingKey生成容器可以这么理解:CodingKey是一种映射关系,旨在生成这种映射关系的容器。此时的容器并不关系内容是什么。
phpvar remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
-
执行encode的时候,才是对该容器的内容填充,此时才用到CodingKey的内容。
phptry remarkContainer.encode(remark.content, forKey: .content)
3. 深入理解CodingKey
encode 需要做这两件事(或不断重复这两个事)
- 生成容器(创建层级结构)
- 容器解码(填充对应数据)
我们来看一个案例,帮助理解这 "两件事":
swift
struct FeedOne: Codable {
var id: Int = 100
var name: String = "xiaoming"
enum CodingKeys: CodingKey {
case id
case name
}
func encode(to encoder: Encoder) throws {
var conrainer = encoder.container(keyedBy: CodingKeys.self)
}
}
let feed = Feed()
guard let value = feed.encode() else { return }
print(value)
//输出信息是一个空字典
{
}
如果我们使用Struct自定义CodingKey
swift
struct FeedOne: Codable {
var id: Int = 100
var name: String = "xiaoming"
struct CustomKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.stringValue = ""
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomKeys.self)
}
}
//输出信息是一个空字典
{
}
经过这两个例子,应该更了解CodingKey。
CodingKey表示一种映射关系或一个规则,通过CodingKey生成的带有这种映射关系的容器。
只有向容器中填充时才在意内容,填充的信息包含两部分, key 和 value。
swift
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
}
swift
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomKeys.self)
try container.encode(id, forKey: .init(stringValue: "id")!)
try container.encode(name, forKey: .init(stringValue: "name")!)
}
无论是enum的CodingKey还是自定义的结构体CodingKey,此时的value都输出为:
json
{
"id" : 100,
"name" : "xiaoming"
}
4. CodingKey的可选设计
CodingKeys
的初始化方法被设计成可选的,是为了处理可能存在的键名不匹配的情况。
当我们从外部数据源(如JSON)中解码数据时,属性名与键名必须一致才能正确地进行解码操作。但是,外部数据源的键名可能与我们的属性名不完全匹配,或者某些键可能在数据源中不存在。通过将CodingKeys
的初始化方法设计为可选的,我们可以在解码过程中处理这些不匹配的情况。
如果某个键在数据源中不存在,我们可以将其设置为nil
,或者使用默认值来填充属性。这样可以确保解码过程的稳定性,并避免由于键名不匹配而导致的解码错误。
六. codingPath
在Swift中,Codable
协议用于将自定义类型与外部数据进行编码和解码。codingPath
是Codable
协议中的一个属性,它表示当前正在编码或解码的属性的路径。
codingPath
是一个数组,它按照嵌套层次结构记录了属性的路径。每个元素都是一个CodingKey
类型的值,它表示当前层级的属性名称。
通过检查codingPath
,您可以了解正在处理的属性的位置,这对于处理嵌套的数据结构非常有用。