这是Swift数据解析计划的系列文章:
Swift数据解析(第一篇) – 技能选型
Swift数据解析(第二篇) – Codable 上
Swift数据解析(第二篇) – Codable 下
Swift数据解析(第三篇) – Codable源码学习
Swift数据解析(第四篇) – SmartCodable 上
Swift数据解析(第四篇) – SmartCodable 下
一. Codable的简略运用
Codable
是 Encodable
和 Decodable
协议的聚合。 同时具有序列化和反序列化的才能。
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: 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(_:)
办法直接解码。假如不知道元素的类型,能够运用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": {}
}
"""
数据回来了空对象。
▿ 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
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 : 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. 三种编码容器
关于三种编码容器SingleValueDecodingContainer, UnkeyedDecodingContainer, KeyedDecodingContainer , 请参阅 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结构,期望将这个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)
。
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
协议用于将自界说类型与外部数据进行编码和解码。codingPath
是Codable
协议中的一个特点,它表明当时正在编码或解码的特点的途径。
codingPath
是一个数组,它依照嵌套层次结构记录了特点的途径。每个元素都是一个CodingKey
类型的值,它表明当时层级的特点称号。
经过查看codingPath
,您能够了解正在处理的特点的方位,这关于处理嵌套的数据结构十分有用。