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

一. Codable的简单使用

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

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. 三种编码容器

关于三种编码容器SingleValueDecodingContainerUnkeyedDecodingContainerKeyedDecodingContainer , 请参考 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属性的示例场景:

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

    php 复制代码
     enum CodingKeys: String, CodingKey {
        case name = "player_name"
        case age
        case nativePlace = "native_Place"
        case scoreInfo
     }
     ​
     var container = encoder.container(keyedBy: CodingKeys.self)
  • 容器解码

    php 复制代码
     try 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实现本层的数据和模型之间的映射关系。

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

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

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

  • 生成容器

    swift 复制代码
     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容器内。

  • 容器解码

    php 复制代码
     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。 即:

    less 复制代码
     RemarkCodingKeys(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是一种映射关系,旨在生成这种映射关系的容器。此时的容器并不关系内容是什么。

    php 复制代码
     var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
  • 执行encode的时候,才是对该容器的内容填充,此时才用到CodingKey的内容。

    php 复制代码
     try 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协议用于将自定义类型与外部数据进行编码和解码。codingPathCodable协议中的一个属性,它表示当前正在编码或解码的属性的路径。

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

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

相关推荐
恋猫de小郭13 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨17 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题18 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
BangRaJun2 天前
LNCollectionView-替换幂率流体
算法·ios·设计
刘小哈哈哈2 天前
iOS 多个输入框弹出键盘处理
macos·ios·cocoa
靴子学长2 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
一如初夏丿2 天前
xcode15 报错 does not contain ‘libarclite‘
ios·xcode
杨武博2 天前
ios 混合开发应用白屏问题
ios
BangRaJun3 天前
LNCollectionView
android·ios·objective-c
二流小码农3 天前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos