Codable 学习笔记
目录
本文为 Raywenderlich 出版的《Expert Swift》第 8 章节《Codable》阅读笔记。
Swift 4.0 就引入了 Codable 协议。话不多说,进入正题:
示例 JSON:
{
"accessKey" : "S|_|p3rs3cr37",
"AddressInfo" : {
"atmCode" : "1132",
"city" : "Albuquerque",
"street" : "3828 Piermont Drive",
"zip" : 87112
},
"ContactInfo" : {
"addedOn" : "04-14-2022 13:58",
"cellularPhone" : "+972 542-288-482",
"email" : "freak4pc@gmail.com",
"homePhone" : "+1 212-741-4695",
"website" : "http://github.com/freak4pc"
},
"family" : [
"Tom",
"Elia"
],
"name" : "Shai Mishali"
}
如何 Decode?
即如何解析这段 JSON?
维持原有层级结构
这个 JSON 对应的 Swift 模型为:
import Foundation
struct Customer1: Codable {
let accessKey: String
let addressInfo: AddressInfo
let contactInfo: ContactInfo
let family: [String]
let name: String
enum CodingKeys: String, CodingKey {
case accessKey
case addressInfo
case contactInfo
case family, name
}
}
// MARK: - AddressInfo
struct AddressInfo: Codable {
let atmCode, city, street: String
let zip: Int
}
// MARK: - ContactInfo
struct ContactInfo: Codable {
let addedOn, cellularPhone, email, homePhone: String
let website: String
}
Customer1 结构体模型的层级结构与 JSON 对象的层级结构保持一致。这样就可以利用 JSONDecoder 把示例 JSON 成功地解析为 Customer1 结构体模型。
自定义 JSON 对应模型的层级结构
以上述 Customer1 模型为例,如果想访问该顾客所在的城市:
let jack = Customer1()
let city = jack.addressInfo.city
如果想直接通过 jack.city 来访问 jack 所在的城市信息,该怎么处理?
1. 使用计算属性
var city: String {
return addressInfo.city
}
但这种方式的弊端是:万一如果想把 AddressInfo 这嵌套的一层直接去掉,得手写好多类似上面的代码。
2. 使用嵌套的 CodingKey 枚举
使用嵌套的 CodingKey 枚举可以把具有层级的 JSON “拍扁”。
enum CodingKeys: String, CodingKey {
case name
case accessKey
case status
case addressInfo = "AddressInfo"
case contactInfo = "ContactInfo"
}
enum AddressInfo: String, CodingKey {
case atmCode
case street
case city
case zip
}
enum ContactInfo: String, CodingKey {
case homePhone, cellularPhone, email, website
case addedOn
}
还得自己实现对应的 decode 方法:
init(from decoder: Decoder) throws {
let customer = try decoder.container(keyedBy: CodingKeys.self)
let name = try customer.decode(String.self, forKey: .name)
let accessKey = try customer.decode(String.self, forKey: .accessKey)
let status = try customer.decode(Int.self, forKey: .status)
self.name = name
self.accessKey = accessKey
let addressInfo = try customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
let atmCode = try addressInfo.decode(String.self, forKey: .atmCode)
let street = try addressInfo.decode(String.self, forKey: .street)
let city = try addressInfo.decode(String.self, forKey: .city)
// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
let zipString = zipString
{
self.zip = Int(zipString) ?? -1
} else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
self.zip = zipInt
} else {
self.zip = -1
}
self.atmCode = atmCode
self.street = street
self.city = city
let contactInfo = try customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
let homePhone = try contactInfo.decode(String.self, forKey: .homePhone)
let cellularPhone = try contactInfo.decode(String.self, forKey: .cellularPhone)
let email = try contactInfo.decode(String.self, forKey: .email)
let website = try contactInfo.decode(String.self, forKey: .website)
let addedOn = try contactInfo.decode(Date.self, forKey: .addedOn)
self.homePhone = homePhone
self.cellularPhone = cellularPhone
self.email = email
self.website = website
self.addedOn = addedOn
}
在自己实现 decode 方法时,还需要对几类容器(Container)有了解:

- Keyed Container:最常见的容器之一,可用来解析键值对数据,对应的 key 定义在
CodingKeys枚举中。 - Unkeyed Container:用来解析不是以字符串为 key 的结构,例如常见的数组结构。
- Single Value Container:用来把单一的数据解析为有具体类型的数据结构。
- Nested Container:当某个容器成为另一个容器的子容器时,它被称为嵌套容器
我们可以方便地把容器理解为:一个对数据编解码的环境。
被“拍扁”后的模型为:
struct Customer2: Encodable {
var name: String
var accessKey: String
var family: [String]
var atmCode: String
var street: String
var city: String
var zip: Int
var homePhone: String
var cellularPhone: String
var email: String
let website: String
let addedOn = Date()
如何 Encode?
示例模型对象:
import Foundation
struct Customer: Encodable {
var name: String
var accessKey: String
var atmCode: String
var street: String
var city: String
var zip: Int
var homePhone: String
var cellularPhone: String
var email: String
let website: String
let addedOn = Date()
var family: [String]
}
let customer = Customer(
name: "Shai Mishali",
accessKey: "S|_|p3rs3cr37",
atmCode: "1132",
street: "3828 Piermont Drive",
city: "Albuquerque",
zip: 87119,
homePhone: "+1 212-741-4695",
cellularPhone: "+972 542-288-482",
email: "freak4pc@gmail.com",
website: "http://github.com/freak4pc",
family: ["Tom", "Elia"]
)
我们可以利用 Encodable 的默认实现来把模型对象转为 JSON 对象:
let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy HH:mm"
return dateFormatter
}()
let jsonEncoder = JSONEncoder()
jsonEncoder.dateEncodingStrategy = .formatted(dateFormatter)
jsonEncoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
if let jsonObject = try? jsonEncoder.encode(customer),
let jsonString = String(data: jsonObject, encoding: .utf8) {
print(jsonString)
}
// 输出:
{
"accessKey" : "S|_|p3rs3cr37",
"addedOn" : "04-14-2022 15:36",
"atmCode" : "1132",
"cellularPhone" : "+972 542-288-482",
"city" : "Albuquerque",
"email" : "freak4pc@gmail.com",
"family" : [
"Tom",
"Elia"
],
"homePhone" : "+1 212-741-4695",
"name" : "Shai Mishali",
"street" : "3828 Piermont Drive",
"website" : "http://github.com/freak4pc",
"zip" : 87119
}
修改 JSON 对象的层级结构
如果想修改 encode 之后的 JSON 对象的层级结构呢?还是得使用嵌套的 CodingKey 枚举,并自己实现 encode 方法:
func encode(to encoder: Encoder) throws {
var customer = encoder.container(keyedBy: CodingKeys.self)
try customer.encode(name, forKey: .name)
try customer.encode(accessKey, forKey: .accessKey)
var addressInfo = customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
try addressInfo.encode(atmCode, forKey: .atmCode)
try addressInfo.encode(street, forKey: .street)
try addressInfo.encode(city, forKey: .city)
try addressInfo.encode(zip, forKey: .zip)
var contactInfo = customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
try contactInfo.encode(homePhone, forKey: .homePhone)
try contactInfo.encode(cellularPhone, forKey: .cellularPhone)
try contactInfo.encode(email, forKey: .email)
try contactInfo.encode(website, forKey: .website)
try contactInfo.encode(addedOn, forKey: .addedOn)
}
达成 Codable
通过上述自定义 encode 和 decode 操作之后,就达成 Codable 。
import Foundation
struct Customer: Codable {
internal init(name: String, accessKey: String, atmCode: String, street: String, city: String, zip: Int, homePhone: String, cellularPhone: String, email: String, website: String, addedOn: Date = Date(), family: [String]) {
self.name = name
self.accessKey = accessKey
self.atmCode = atmCode
self.street = street
self.city = city
self.zip = zip
self.homePhone = homePhone
self.cellularPhone = cellularPhone
self.email = email
self.website = website
self.addedOn = addedOn
self.family = family
}
var name: String
var accessKey: String
var atmCode: String
var street: String
var city: String
var zip: Int
var homePhone: String
var cellularPhone: String
var email: String
let website: String
var addedOn: Date = Date()
var family: [String]
enum CodingKeys: String, CodingKey {
case name
case accessKey
case family
case addressInfo = "AddressInfo"
case contactInfo = "ContactInfo"
}
enum AddressInfo: String, CodingKey {
case atmCode
case street
case city
case zip
}
enum ContactInfo: String, CodingKey {
case homePhone, cellularPhone, email, website
case addedOn
}
func encode(to encoder: Encoder) throws {
var customer = encoder.container(keyedBy: CodingKeys.self)
try customer.encode(name, forKey: .name)
try customer.encode(accessKey, forKey: .accessKey)
var family = customer.nestedUnkeyedContainer(forKey: .family)
try family.encode(contentsOf: self.family)
var addressInfo = customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
try addressInfo.encode(atmCode, forKey: .atmCode)
try addressInfo.encode(street, forKey: .street)
try addressInfo.encode(city, forKey: .city)
try addressInfo.encode(zip, forKey: .zip)
var contactInfo = customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
try contactInfo.encode(homePhone, forKey: .homePhone)
try contactInfo.encode(cellularPhone, forKey: .cellularPhone)
try contactInfo.encode(email, forKey: .email)
try contactInfo.encode(website, forKey: .website)
try contactInfo.encode(addedOn, forKey: .addedOn)
}
init(from decoder: Decoder) throws {
let customer = try decoder.container(keyedBy: CodingKeys.self)
let name = try customer.decode(String.self, forKey: .name)
let accessKey = try customer.decode(String.self, forKey: .accessKey)
self.name = name
self.accessKey = accessKey
let addressInfo = try customer.nestedContainer(keyedBy: AddressInfo.self, forKey: .addressInfo)
let atmCode = try addressInfo.decode(String.self, forKey: .atmCode)
let street = try addressInfo.decode(String.self, forKey: .street)
let city = try addressInfo.decode(String.self, forKey: .city)
// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
let zipString = zipString
{
self.zip = Int(zipString) ?? -1
} else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
self.zip = zipInt
} else {
self.zip = -1
}
self.atmCode = atmCode
self.street = street
self.city = city
let contactInfo = try customer.nestedContainer(keyedBy: ContactInfo.self, forKey: .contactInfo)
let homePhone = try contactInfo.decode(String.self, forKey: .homePhone)
let cellularPhone = try contactInfo.decode(String.self, forKey: .cellularPhone)
let email = try contactInfo.decode(String.self, forKey: .email)
let website = try contactInfo.decode(String.self, forKey: .website)
let addedOn = try contactInfo.decode(Date.self, forKey: .addedOn)
self.homePhone = homePhone
self.cellularPhone = cellularPhone
self.email = email
self.website = website
self.addedOn = addedOn
let family = try customer.decode([String].self, forKey: .family)
self.family = family
}
}
可以看出:自定义实现 encode 和 decode 方法所带来的代码量还是不小的,然而于此同时也带来了较高的灵活度。
举个例子,假如 JSON 对象中 zip 对应的值的类型是字符串,如“87119”。而 Customer 模型中 zip 变量类型为 Int,则可以在自定义 decode 方法中特别处理:
// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理
if case let zipString? = try? addressInfo.decodeIfPresent(String.self, forKey: .zip),
let zipString = zipString
{
self.zip = Int(zipString) ?? -1
} else if let zipInt = try addressInfo.decodeIfPresent(Int.self, forKey: .zip) {
self.zip = zipInt
} else {
self.zip = -1
}
再者,如果 JSON 新加一个 memberType 字段来表示顾客的身份:
enum MemberType: Int {
case visitor = 1
case vip = 2
case unknown = -1
}
突然某一天,收到的 JSON 对象中 memberType 为 3,意味超级 vip 客户,这是业务迭代新增的需求,而在 Customer 未对此改动作相应的更新时,也在 decode 方法里对这种情况作兼容处理,比如直接设为 .unknown。
JSONEncoder
- KeyEncodingStrategy:内置默认的
convertToSnakeCase策略,如模型中的变量名为:xmlContents,经过convertToSnakeCase策略之后,对应的 JSON key 会变为xml_contents。也提供自定义的接口,让开发者自行决定。 - OutputFormatting:内置三种默认的输出格式化方式
- prettyPrinted:iOS 7 开始支持
- sortedKeys:iOS 11 开始支持
- withoutEscapingSlashes:iOS 13 开始支持
- userInfo:外部可通过此与自定义的 encode 过程进行信息传递。
- DateEncodingStrategy:内置
iso8601等编码格式,同时也提供传入自定义的DateFormatter对象对日期进行自定义处理。 - DataEncodingStrategy:内置
base64等编码格式,同时也提供自定义接口,让开发者自行决定。
JSONDecoder 的结构与 JSONEncoder 很相似,不再一一赘述。
SwiftyJSON 源码分析(未完待续)
