Swift数据解析(第二篇) - Codable 上

2,777 阅读20分钟

这是Swift数据解析方案的系列文章:

Swift数据解析(第一篇) - 技术选型

Swift数据解析(第二篇) - Codable 上

Swift数据解析(第二篇) - Codable 下

Swift数据解析(第三篇) - Codable源码学习

Swift数据解析(第四篇) - SmartCodable 上

Swift数据解析(第四篇) - SmartCodable 下

一. Codable的简单使用

CodableEncodableDecodable 协议的聚合。 同时拥有序列化和反序列化的能力。

  public protocol Encodable {
      func encode(to encoder: Encoder) throws
  }
  
  public protocol Decodable {
      init(from decoder: Decoder) throws
  }
  
  public typealias Codable = Decodable & Encodable

使用起来很简单:

  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协议,写了挺多演示代码,为了减少示例中的代码量,封装了编码和解码的方法,演示代码将直接使用这四个方法。

  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 协议。

  public protocol Decodable {
    init(from decoder: Decoder) throws
  }
  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。

  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方法的原因是,它的主要目的是解码单个值,而不是处理可选值。它假设解码的值始终存在,并且如果解码失败,会抛出一个错误。

  public protocol SingleValueDecodingContainer {
  
    var codingPath: [CodingKey] { get }
  
    /// 解码空值时返回true
    func decodeNil() -> Bool
  
    /// 解码给定类型的单个值。
    func decode<T>(_ type: T.Type) throws -> T where T : Decodable
  }

使用起来也很简单。可以使用decode(_:)方法直接解码。

  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 属性检查是否已经遍历完所有元素。

 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

 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]

如果继续解码将报错:

 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 用于解码键值对类型的数据。它提供了一种通过键从容器中提取值的方式。可以借助字典理解。

 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。一次出行只会选择其中的一种,即服务端只会下发一种数据。

步行出行:

 {
    "travelTool": "walk",
    "walk": {
        "hours": 3,
    }
 }

骑行出行:

 {
    "travelTool": "riding",
    "riding": {
        "hours":2,
        "attention": "注意带头盔"
    }
 }

公共交通出行:

 {
    "travelTool": "publicTransport",
    "publicTransport": {
        "hours": 1,
        "transferTimes": "3",
    }
 }

通过重写init(from decoder: Decoder) 方法,使用decodeIfPresent处理动态键值结构。

 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策略,用于在解码之前自动更改密钥值的策略。它一个枚举值,提供了三种策略:

 public enum KeyDecodingStrategy : Sendable {
 ​
    /// 使用每种类型指定的键,默认策略。
    case useDefaultKeys
 ​
    /// 使用小驼峰策略
    case convertFromSnakeCase
 ​
    /// 使用自定义策略
    @preconcurrency case custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey)
 }

使用起来也比较简单

 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

演示样例

 struct Feed: Codable {
    var name: String
 }

正常的json数据返回是这样的:

 let json = """
 {
  "name": "小明"
 }
 """
 ​
 guard let jsonData = json.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: "小明")

我们将基于这个数据模型,对以下3种特殊数据场景寻找解决方案。

1. 类型键不存在

 let json = """
 {
 ​
 }
 """

数据返回了空对象。

 ▿ DecodingError
  ▿ keyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "name", intValue: nil)
    ▿ .1 : Context
      - codingPath : 0 elements
      - debugDescription : "No value associated with key CodingKeys(stringValue: "name", intValue: nil) ("name")."
      - underlyingError : nil

2. 值为null

 let json = """
 {
  "name": null
 }
 """

数据返回了类型不匹配的值。

  DecodingError
   valueNotFound : 2 elements
    - .0 : Swift.String
     .1 : Context
       codingPath : 1 element
        - 0 : CodingKeys(stringValue: "name", intValue: nil)
      - debugDescription : "Expected String value but found null instead."
      - underlyingError : nil

3. 数据值为null

 let json = """
 {
  "name": 2
 }
 """

数据返回了null值。

  DecodingError
   typeMismatch : 2 elements
    - .0 : Swift.String
     .1 : Context
       codingPath : 1 element
        - 0 : CodingKeys(stringValue: "name", intValue: nil)
      - debugDescription : "Expected to decode String but found a number instead."
      - underlyingError : nil

如何兼容?

 struct Feed: Codable {
    var name: String
    var id: Int
     
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // 1.数据中是否否包含该键
        if container.contains(.name) {
            // 2.是否为nil
            if try container.decodeNil(forKey: .name) {
                self.name = ""
            } else {
                do {
                    // 3.是否类型正确
                    self.name = try container.decode(String.self, forKey: .name)
                } catch {
                    self.name = ""
                }
            }
        } else {
            self.name = ""
        }
    }
 }

一个属性的完整兼容应该包含以下三个步骤:

  • 该字段是否存在 container.contains(.name)
  • 数据是否为null try container.decodeNil(forKey: .name)
  • 是否类型匹配 try container.decode(String.self, forKey: .name)

可以想象如果一个模型有上百个字段的场景(对于我们这样做数据业务的app非常常见)。

类型键不存在数据值是null 虽然可以通过可选类型避免,但是类型不匹配的情况,只能重写协议方法来避免。你可以想象一下这种痛苦。

使用可选势必会有大量的可选绑定,对于 enum 和 Bool 的可选的使用是非常痛苦的。

三. Encodable

  public protocol Encodable {
    func encode(to encoder: Encoder) throws
  }
  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。

  public enum EncodingError : Error {
    case invalidValue(Any, EncodingError.Context)
  }

此Error就只有一种情况: 表示要编码的值无效或不符合预期的格式要求。

2. 三种编码容器

关于三种编码容器SingleValueEncodingContainerUnkeyedEncodingContainerKeyedEncodingContainer , 请参考 Decodable 的介绍。

3.编码时的 Optional值

Person模型中的name属性是可选值,并设置为了nil。进行encode的时候,不会以 null 写入json中。

  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。等同于:

  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属性的示例场景:

  1. 自定义解码行为:可以使用userInfo属性传递自定义的解码选项或配置给Decoder。例如,您可以在userInfo中设置一个布尔值,以指示解码器在遇到特定的键时执行特殊的解码逻辑。
  2. 传递上下文信息:如果需要在解码过程中访问一些上下文信息,例如用户身份验证令牌或当前语言环境设置。
  3. 错误处理:可以在userInfo中存储有关错误处理的信息。例如可以将一个自定义的错误处理器存储在userInfo中,以便在解码过程中捕获和处理特定类型的错误。
  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会使用键来匹配编码的数据,并将数据解码为相应的属性值。
  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

  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,用来处理不匹配的属性映射关系。还可以使用结构体来实现,提供更灵活的使用,处理复杂的层级结构。

  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结构扁平化的解析到Player结构体中。

  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")
                ]
  )
  */

实现逻辑是这样的:

  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)
        }
    }
  }

CodingKey.png

1. 解码最外层数据

层级结构:包含4个key的字典结构。

  {
    "player_name": ...
    "age": ...
    "native_Place": ...
    "scoreInfo": ...    
  }
  • 生成容器

    根据这个结构,使用这个CodingKeys实现数据和模型之间的映射关系。

      enum CodingKeys: String, CodingKey {
        case name = "player_name"
        case age
        case nativePlace = "native_Place"
        case scoreInfo
      }
      
      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)
      
      // scoreInfo是下一层的数据
    
2. 解码scoreInfo层数据

层级结构:包含3个key的字典结构。

  {
    "gross_score": ...
    "scores": ...
    "remarks": ...
  }
  • 生成容器

    根据这个结构,我们使用这个ScoreInfoCodingKeys实现本层的数据和模型之间的映射关系。

      enum ScoreInfoCodingKeys: String, CodingKey {
        case grossScore = "gross_score"
        case scores
        case remarks
      }
      var scoresContainer = container.nestedContainer(keyedBy: ScoreInfoCodingKeys.self, forKey: .scoreInfo)
    
  • 容器编码

      try scoresContainer.encode(grossScore, forKey: .grossScore)
      try scoresContainer.encode(scores, forKey: .scores)
      
      // remarks层是下层的数据
    
3. 解码remarks层数据

我们期望进行这样的层级转换

CodingKey-数据转换.png

  • 生成容器

      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")!
      }
      var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
    

    使用scoresContainer容器生成一个新的remarksContainer容器,表示remarksContainer容器是在scoresContainer容器内。

  • 容器解码

      for 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。 即:

      RemarkCodingKeys(stringValue: "judgeTwo", intValue: nil)
      RemarkCodingKeys(stringValue: "judgeOne", intValue: nil)
      RemarkCodingKeys(stringValue: "judgeThree", intValue: nil)
    

    最后将数据填充到remarkContainer里面: try remarkContainer.encode(remark.content, forKey: .content)

3. 深入理解CodingKey

我们来看这样一个案例

  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

  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)
    }
  }
  
  //输出信息是一个空字典
  {
  
  }

我们进行encode的时候,只使用CodingKeys创建了容器,并没有进行进一步的编码。此时print点输出是:空字典。

向容器中填充(开始编码:包含两部分 key 和 value)

  func encode(to encoder: Encoder) throws {
    // 使用枚举类型的CodingKeys
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(name, forKey: .name)
  }
  func encode(to encoder: Encoder) throws {
    // 使用结构体类型的CodingKeys
    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都输出为:

  {
  "id" : 100,
  "name" : "xiaoming"
  }

我有意的将编码过程拆分成两部分: 生成容器在容器中编码

 // MARK: - Encoder Methods
 public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
    // If an existing keyed container was already requested, return that one.
    let topContainer: NSMutableDictionary
    if self.canEncodeNewValue {
        // We haven't yet pushed a container at this level; do so here.
        topContainer = self.storage.pushKeyedContainer()
    } else {
        guard let container = self.storage.containers.last as? NSMutableDictionary else {
            preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.")
        }
 ​
        topContainer = container
    }
 ​
    let container = _JSONKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)
    return KeyedEncodingContainer(container)
 }

在Codable的源码中,我们可以看到 func container<Key>(keyedBy: Key.Type) 方法中并没有直接使用 keyedBy 参数,而是使用 Key 初始化let container = _JSONKeyedEncodingContainer<Key>(referencing: self, codingPath: self.codingPath, wrapping: topContainer)。 CodingKey的定义传递到 _JSONKeyedEncodingContainer

在编码过程中,直接从所在容器中,进行一一对应的编码。

 public mutating func encode(_ value: String) throws { 
      self.container.add(self.encoder.box(value)) 
 }
 /// container是一个容器,按照嵌套关系,存储编码数据。
 /// A reference to the container we're writing to.
 //private let container: NSMutableArray
 ​
 fileprivate func box(_ value: String) -> NSObject {
      return NSString(string: value) 
 }

4. CodingKey的可选设计

CodingKeys的初始化方法被设计成可选的,是为了处理可能存在的键名不匹配的情况。

当我们从外部数据源(如JSON)中解码数据时,属性名与键名必须一致才能正确地进行解码操作。但是,外部数据源的键名可能与我们的属性名不完全匹配,或者某些键可能在数据源中不存在。通过将CodingKeys的初始化方法设计为可选的,我们可以在解码过程中处理这些不匹配的情况。

如果某个键在数据源中不存在,我们可以将其设置为nil,或者使用默认值来填充属性。这样可以确保解码过程的稳定性,并避免由于键名不匹配而导致的解码错误。

六. codingPath

在Swift中,Codable协议用于将自定义类型与外部数据进行编码和解码。codingPathCodable协议中的一个属性,它表示当前正在编码或解码的属性的路径。

codingPath是一个数组,它按照嵌套层次结构记录了属性的路径。每个元素都是一个CodingKey类型的值,它表示当前层级的属性名称。

通过检查codingPath,您可以了解正在处理的属性的位置,这对于处理嵌套的数据结构非常有用。