一. 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 : nilKeyedDecodingContainer
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,您可以了解正在处理的属性的位置,这对于处理嵌套的数据结构非常有用。