这是Swift数据解析计划的系列文章:

Swift数据解析(第一篇) – 技能选型

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

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

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

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

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

一. Codable的简略运用

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

publicprotocolEncodable{
 funcencode(toencoder:Encoder)throws
}
​
publicprotocolDecodable{
 init(fromdecoder:Decoder)throws
}
​
publictypealiasCodable=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: Stringpublic 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(_:)办法直接解码。假如不知道元素的类型,能够运用decodeNil()办法查看元素是否为nil

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 hobby: Hobby
}
struct Hobby: Codable {
    var name: String
    var year: Int
}

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

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. 类型键不存在

let json = """
{
   "hobby": {}
}
"""

数据回来了空对象。

DecodingErrorkeyNotFound : 2 elements
    - .0 : CodingKeys(stringValue: "name", intValue: nil)
    ▿ .1 : ContextcodingPath : 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
struct Hobby: Codable {
    var name: String?
    var year: Int?
}

将Hobby的特点设置为可选类型,会被主动解析为nil。会输出:Feed(hobby: CodableTest.Hobby(name: nil, year: nil))

兼容计划2
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. 类型键不匹配

let json = """
{
   "hobby": "basketball"
}
"""

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

 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

兼容计划:

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

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

数据回来了null值。

 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:

struct Feed: Codable {
    var hobby: Hobby?
}

将hobby设置为可选值,Codable会主动将null映射为nil,很简略就解决了这个问题。

兼容计划2:

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

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 : CodingKeyfunc unkeyedContainer() -> UnkeyedEncodingContainerfunc singleValueContainer() -> SingleValueEncodingContainer
}

Encodable协议相对Decodable协议较为简略,请参阅Decodable篇。

1. EncodingError

编码失利,就会抛出反常,此刻的error便是EncodingError。

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

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

2. 三种编码容器

关于三种编码容器SingleValueDecodingContainerUnkeyedDecodingContainerKeyedDecodingContainer , 请参阅 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结构,期望将这个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)
   }
 }
}

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

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层数据

咱们期望进行这样的层级转化

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

  • 生成容器

    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)

4. 总结
  • CodingKey即能够是枚举,又能够是结构体。

  • 经过CodingKey生成容器能够这么了解:CodingKey是一种映射联系,旨在生成这种映射联系的容器。此刻的容器并不联系内容是什么。

    var remarksContainer = scoresContainer.nestedContainer(keyedBy: RemarkCodingKeys.self, forKey: .remarks)
    
  • 履行encode的时分,才是对该容器的内容填充,此刻才用到CodingKey的内容。

    try remarkContainer.encode(remark.content, forKey: .content)
    

3. 深化了解CodingKey

encode 需求做这两件事(或不断重复这两个事)

  • 生成容器(创立层级结构)
  • 容器解码(填充对应数据)

咱们来看一个案例,协助了解这 “两件事”:

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

经过这两个比如,应该更了解CodingKey。

CodingKey表明一种映射联系或一个规矩,经过CodingKey生成的带有这种映射联系的容器。

只要向容器中填充时才介意内容,填充的信息包括两部分, key 和 value。

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)
}
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都输出为:

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

4. CodingKey的可选规划

CodingKeys的初始化办法被规划成可选的,是为了处理或许存在的键名不匹配的状况。

当咱们从外部数据源(如JSON)中解码数据时,特点名与键名有必要共同才能正确地进行解码操作。可是,外部数据源的键名或许与咱们的特点名不完全匹配,或许某些键或许在数据源中不存在。经过将CodingKeys的初始化办法规划为可选的,咱们能够在解码进程中处理这些不匹配的状况。

假如某个键在数据源中不存在,咱们能够将其设置为nil,或许运用默许值来填充特点。这样能够保证解码进程的稳定性,并防止因为键名不匹配而导致的解码过错。

六. codingPath

在Swift中,Codable协议用于将自界说类型与外部数据进行编码和解码。codingPathCodable协议中的一个特点,它表明当时正在编码或解码的特点的途径。

codingPath是一个数组,它依照嵌套层次结构记录了特点的途径。每个元素都是一个CodingKey类型的值,它表明当时层级的特点称号。

经过查看codingPath,您能够了解正在处理的特点的方位,这关于处理嵌套的数据结构十分有用。