0%

Codable 学习笔记

本文为 Raywenderlich 出版的《Expert Swift》第 8 章节《Codable》阅读笔记。

Swift 4.0 就引入了 Codable 协议。话不多说,进入正题:
示例 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"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 模型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 模型为例,如果想访问该顾客所在的城市:

1
2
let jack = Customer1()
let city = jack.addressInfo.city

如果想直接通过 jack.city 来访问 jack 所在的城市信息,该怎么处理?

1. 使用计算属性

1
2
3
var city: String {
return addressInfo.city
}

但这种方式的弊端是:万一如果想把 AddressInfo 这嵌套的一层直接去掉,得手写好多类似上面的代码。

2. 使用嵌套的 CodingKey 枚举

使用嵌套的 CodingKey 枚举可以把具有层级的 JSON “拍扁”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)有了解:
codable-01

  • Keyed Container:最常见的容器之一,可用来解析键值对数据,对应的 key 定义在 CodingKeys 枚举中。
  • Unkeyed Container:用来解析不是以字符串为 key 的结构,例如常见的数组结构。
  • Single Value Container:用来把单一的数据解析为有具体类型的数据结构。
  • Nested Container:当某个容器成为另一个容器的子容器时,它被称为嵌套容器

我们可以方便地把容器理解为:一个对数据编解码的环境。

被“拍扁”后的模型为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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?

示例模型对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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 方法中特别处理:

1
2
3
4
5
6
7
8
9
10
// 当 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 字段来表示顾客的身份:

1
2
3
4
5
enum MemberType: Int {
case vistor = 1
case vip = 2
case unknow = -1
}

突然某一天,收到的 JSON 对象中 memberType 为 3,意味超级 vip 客户,这是业务迭代新增的需求,而在 Customer 未对此改动作相应的更新时,也在 decode 方法里对这种情况作兼容处理,比如直接设为 .unknow

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 源码分析(未完待续)