这是Swift数据解析方案的系列文章:
Swift数据解析(第四篇) - SmartCodable 上
Swift数据解析(第四篇) - SmartCodable 下
一. Codable的简单使用
Codable
是 Encodable
和 Decodable
协议的聚合。 同时拥有序列化和反序列化的能力。
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. 三种编码容器
关于三种编码容器SingleValueEncodingContainer, UnkeyedEncodingContainer, KeyedEncodingContainer , 请参考 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属性的示例场景:
- 自定义解码行为:可以使用userInfo属性传递自定义的解码选项或配置给Decoder。例如,您可以在userInfo中设置一个布尔值,以指示解码器在遇到特定的键时执行特殊的解码逻辑。
- 传递上下文信息:如果需要在解码过程中访问一些上下文信息,例如用户身份验证令牌或当前语言环境设置。
- 错误处理:可以在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)
}
}
}
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层数据
我们期望进行这样的层级转换
-
生成容器
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
协议用于将自定义类型与外部数据进行编码和解码。codingPath
是Codable
协议中的一个属性,它表示当前正在编码或解码的属性的路径。
codingPath
是一个数组,它按照嵌套层次结构记录了属性的路径。每个元素都是一个CodingKey
类型的值,它表示当前层级的属性名称。
通过检查codingPath
,您可以了解正在处理的属性的位置,这对于处理嵌套的数据结构非常有用。