Swift Codable 解码问题全攻略|多场景 JSON 解析与错误容忍技巧
针对 iOS 开发中 Swift Codable 解码常见困境,提供多场景 JSON 解析实战范例,解决栏位多型、阵列错误容忍与自订日期格式等问题,助你打造稳健高效的资料映射流程。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
现实使用 Codable 上遇到的 Decode 问题场景总汇(上)
从基础到进阶,深入使用 Decodable 满足所有可能会遇到的问题场景
Photo by Gustas Brazaitis
前言
因应后端 API 升级需要调整 API 处理架构,近期趁这个机会一并将原本使用 Objective-C 撰写的网路处理架构更新成 Swift;因语言不同,也不在适合使用原本的 Restkit 帮我们处理网路层应用,但不得不说 Restkit 的功能包山包海非常强大,在专案中也用得活灵活现,基本没有太大的问题;但相对的非常笨重、几乎已不再维护、纯 Objective-C;未来势必也要更换的。
Restkit 几乎帮我们处理完所有网路请求相关会需要到的功能,从基本的网路处理、API 呼叫、网路处理,到 Response 处理 JSON String to Object 甚至是 Object 存入 Core Data 它都能一起处理实打实的一个 Framework 打十个。
随著时代的演进,目前的 Framework 已不在主打一个包全部,更多的是灵活、轻巧、组合,增加更多弹性创造更多变化;因此再替换成 Swift 语言的同时,我们选择使用 Moya 作为网路处理部分的套件,其他我们需要的功能再选择其他方式进行组合。
正题
关于 JSON String to Object Mapping 部分,我们使用 Swift 自带的 Codable (Decodable) 协议 & JSONDecoder 进行处理;并拆分 Entity/Model 加强权责区分、操作及阅读性、另外 Code Base 混 Objective-C 和 Swift 也要考量进去。
* Encodable 的部份省略、范例均只展示实作 Decodable,大同小异,可以 Decode 基本也能 Encode。
开始
假设我们初始的 API Response JSON String 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"id": 123456,
"comment": "是告五人,不是五告人!",
"target_object": {
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
"commenter": {
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "zhgchgli@gmail.com"
}
}
由上范例我们可以拆成:User/Song/Comment 三个 Entity & Model,让我们组合能复用,为方便展示先将 Entity/Model 写在同个档案。
User:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Entity:
struct UserEntity: Decodable {
var id: Int
var name: String
var email: String
}
//Model:
class UserModel: NSObject {
init(_ entity: UserEntity) {
self.id = entity.id
self.name = entity.name
self.email = entity.email
}
var id: Int
var name: String
var email: String
}
Song:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Entity:
struct SongEntity: Decodable {
var id: Int
var name: String
}
//Model:
class SongModel: NSObject {
init(_ entity: SongEntity) {
self.id = entity.id
self.name = entity.name
}
var id: Int
var name: String
}
Comment:
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
// Entity:
struct CommentEntity: Decodable {
enum CodingKeys: String, CodingKey {
case id
case comment
case targetObject = "target_object"
case commenter
}
var id: Int
var comment: String
var targetObject: SongEntity
var commenter: UserEntity
}
//Model:
class CommentModel: NSObject {
init(_ entity: CommentEntity) {
self.id = entity.id
self.comment = entity.comment
self.targetObject = SongModel(entity.targetObject)
self.commenter = UserModel(entity.commenter)
}
var id: Int
var comment: String
var targetObject: SongModel
var commenter: UserModel
}
JSONDecoder:
1
2
3
4
5
6
7
let jsonString = "{ \"id\": 123456, \"comment\": \"是告五人,不是五告人!\", \"target_object\": { \"type\": \"song\", \"id\": 99, \"name\": \"披星戴月的想你\" }, \"commenter\": { \"type\": \"user\", \"id\": 1, \"name\": \"zhgchgli\", \"email\": \"zhgchgli@gmail.com\" } }"
let jsonDecoder = JSONDecoder()
do {
let result = try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)
} catch {
print(error)
}
CodingKeys Enum?
当我们的 JSON String Key Name 与 Entity Object Property Name 不相匹配时可以在内部加一个 CodingKeys 枚举进行对应,毕竟后端资料源的 Naming Convention 不是我们可以控制的。
1
2
case PropertyKeyName = "后端栏位名称"
case PropertyKeyName //不指定则预设使用 PropertyKeyName 为后端栏位名称
一旦加入 CodingKeys 枚举,则必须列举出所有非 Optional 的栏位,不能只列举想要客制的 Key。
另外一种方式是设定 JSONDecoder 的 keyDecodingStrategy,若 Response 资料栏位与 Property Name 仅为 snake_case
<-> camelCase
区别,可直接设定 .keyDecodingStrategy
= .convertFromSnakeCase
就能自动匹配 Mapping。
1
2
3
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)
回传资料是阵列时:
1
2
3
struct SongListEntity: Decodable {
var songs:[SongEntity]
}
为 String 加上约束:
1
2
3
4
5
6
7
8
9
10
11
struct SongEntity: Decodable {
var id: Int
var name: String
var type: SongType
enum SongType {
case rock
case pop
case country
}
}
适用于有限范围的字串类型,写成 Enum 方便我们传递、使用;若出现为列举的值会 Decode 失败!
善用泛型包裹固定结构:
假设多笔回传的 JSON String 固定格式为:
1
2
3
4
5
6
7
8
9
10
11
12
{
"count": 10,
"offset": 0,
"limit": 0,
"results": [
{
"type": "song",
"id": 1,
"name": "1"
}
]
}
即可用泛型方式包裹起来:
1
2
3
4
5
6
struct PageEntity<E: Decodable>: Decodable {
var count: Int
var offset: Int
var limit: Int
var results: [E]
}
使用: PageEntity<Song>.self
Date/Timestamp 自动 Decode:
设定 JSONDecoder
的 dateDecodingStrategy
.secondsSince1970/.millisecondsSince1970
: unix timestamp.deferredToDate
: 苹果的 timestamp,罕用,不同于 unix timestamp,这是从 2001/01/01 起算.iso8601
: ISO 8601 日期格式.formatted(DateFormatter)
: 依照传入的 DateFormatter Decode Date.custom
: 自订 Date Decode 逻辑
.cutstom 范例:假设 API 会回传 YYYY/MM/DD 和 ISO 8601 两种格式,两中都要能 Decode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var dateFormatter = DateFormatter()
var iso8601DateFormatter = ISO8601DateFormatter()
let decoder: JSONDecoder = JSONDecoder()
decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
//ISO8601:
if let date = iso8601DateFormatter.date(from: dateString) {
return date
}
//YYYY-MM-DD:
dateFormatter.dateFormat = "yyyy-MM-dd"
if let date = dateFormatter.date(from: dateString) {
return date
}
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
})
let result = try jsonDecoder.decode(CommentEntity.self, from: jsonString.data(using: .utf8)!)
*DateFormatter 在 init 时非常消耗性能,尽可能重复使用。
基本 Decode 常识:
Decodable Protocol 内的的栏位类型(struct/class/enum),都须实作 Decodable Protocol;亦或是在 init decoder 时赋予值
栏位类型不相符时会 Decode 失败
Decodable Object 中栏位设为 Optional 的话则为可有可无,有给就 Decode
Optional 栏位可接受: JSON String 无栏位、有给但给 nil
空白、0 不等于 nil,nil 是 nil;弱型别的后端 API 需注意!
预设 Decodable Object 中有列举且非 Optional 的栏位,若 JSON String 没给会 Decode 失败(后续会说明如何处理)
预设 遇到 Decode 失败会直接中断跳出,无法单纯跳过有误的资料(后续会说明如何处理)
进阶使用
到此为止基本的使用已经完成了,但现实世界不会那么简单;以下列举几个进阶会遇到的场景并提出适用 Codable 的解决方案,从这边开始我们就无法靠原始的 Decode 帮我们补 Mapping 了,要自行实作 init(from decoder: Decoder)
客制 Decode 操作。
*这边暂时先只展示 Entity 的部分,Model 还用不到。
init(from decoder: Decoder)
init decoder,必须赋予所有非 Optional 的栏位初始值(就是 init 啦!)。
自订 Decode 操作时,我们需要从 decoder
中取得 container
出来操作取值, container
有三种取得内容的类型。
第一种 container(keyedBy: CodingKeys.self) 依照 CodingKeys 操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct SongEntity: Decodable {
var id: Int
var name: String
enum CodingKeys: String, CodingKey {
case id
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
//参数 1 接受支援:实作 Decodable 的类别
//参数 2 CodingKeys
self.name = try container.decode(String.self, forKey: .name)
}
}
第二种 singleValueContainer 将整包取出操作(单值):
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
enum HandsomeLevel: Decodable {
case handsome(String)
case normal(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let name = try container.decode(String.self)
if name == "zhgchgli" {
self = .handsome(name)
} else {
self = .normal(name)
}
}
}
struct UserEntity: Decodable {
var id: Int
var name: HandsomeLevel
var email: String
enum CodingKeys: String, CodingKey {
case id
case name
case email
}
}
适用于 Associated Value Enum 栏位类型,例如 name 还自带帅气程度!
第三种 unkeyedContainer 将整包视为一包阵列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ListEntity: Decodable {
var items:[Decodable]
init(from decoder: Decoder) throws {
var unkeyedContainer = try decoder.unkeyedContainer()
self.items = []
while !unkeyedContainer.isAtEnd {
//unkeyedContainer 内部指针会自动在 decode 操作后指向下一个对象
//直到指向结尾即代表遍历结束
if let id = try? unkeyedContainer.decode(Int.self) {
items.append(id)
} else if let name = try? unkeyedContainer.decode(String.self) {
items.append(name)
}
}
}
}
let jsonString = "[\"test\",1234,5566]"
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode(ListEntity.self, from: jsonString.data(using: .utf8)!)
print(result)
适用不固定类型的阵列栏位。
Container 之下我们还能使用 nestedContainer / nestedUnkeyedContainer 对特定栏位操作:
*将资料栏位扁平化(类似 flatMap)
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
struct ListEntity: Decodable {
enum CodingKeys: String, CodingKey {
case items
case date
case name
case target
}
enum PredictKey: String, CodingKey {
case type
}
var date: Date
var name: String
var items: [Decodable]
var target: Decodable
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.date = try container.decode(Date.self, forKey: .date)
self.name = try container.decode(String.self, forKey: .name)
let nestedContainer = try container.nestedContainer(keyedBy: PredictKey.self, forKey: .target)
let type = try nestedContainer.decode(String.self, forKey: .type)
if type == "song" {
self.target = try container.decode(SongEntity.self, forKey: .target)
} else {
self.target = try container.decode(UserEntity.self, forKey: .target)
}
var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .items)
self.items = []
while !unkeyedContainer.isAtEnd {
if let song = try? unkeyedContainer.decode(SongEntity.self) {
items.append(song)
} else if let user = try? unkeyedContainer.decode(UserEntity.self) {
items.append(user)
}
}
}
}
存取、Decode 不同阶层的物件,范例展示 target/items 使用 nestedContainer flat 出 type 再依照 type 去做对应的 decode。
Decode & DecodeIfPresent
DecodeIfPresent: Response 有给资料栏位时才会进行 Decode(Codable Property 设 Optional 时)
Decode:进行 Decode 操作,若 Response 无给资料栏位会抛出 Error
*以上只是简单介绍一下 init decoder、container 有哪些方法、功能,看不懂也没关系,我们直接进入现实场景;在范例中感受组合起来的操作方式。
现实场景
回到原本的范例 JSON String。
场景1. 假设今天对谁留言可能是对歌曲或对人留言, targetObject
栏位可能的对象是 User
或 Song
? 那该如何处理?
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
{
"results": [
{
"id": 123456,
"comment": "是告五人,不是五告人!",
"target_object": {
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
"commenter": {
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "zhgchgli@gmail.com"
}
},
{
"id": 55,
"comment": "66666!",
"target_object": {
"type": "user",
"id": 1,
"name": "zhgchgli"
},
"commenter": {
"type": "user",
"id": 2,
"name": "aaaa",
"email": "aaaa@gmail.com"
}
}
]
}
方式 a.
使用 Enum 做为容器 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
41
42
43
struct CommentEntity: Decodable {
enum CodingKeys: String, CodingKey {
case id
case comment
case targetObject = "target_object"
case commenter
}
var id: Int
var comment: String
var targetObject: TargetObject
var commenter: UserEntity
enum TargetObject: Decodable {
case song(SongEntity)
case user(UserEntity)
enum PredictKey: String, CodingKey {
case type
}
enum TargetObjectType: String, Decodable {
case song
case user
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PredictKey.self)
let singleValueContainer = try decoder.singleValueContainer()
let targetObjectType = try container.decode(TargetObjectType.self, forKey: .type)
switch targetObjectType {
case .song:
let song = try singleValueContainer.decode(SongEntity.self)
self = .song(song)
case .user:
let user = try singleValueContainer.decode(UserEntity.self)
self = .user(user)
}
}
}
}
我们将 targetObject
的属性换成 Associated Value Enum,在 Decode 时才决定 Enum 内要放什么内容。
核心实践是建立一个符合 Decodable 的 Enum 做为容器,decode 时先取关键栏位出来判断(范例 JSON String 中的 type
栏位),若为 Song
则使用 singleValueContainer 将整包解成 SongEntity
,若为 User
亦然。
要使用时再从 Enum 中取出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//if case let
if case let CommentEntity.TargetObject.user(user) = result.targetObject {
print(user)
} else if case let CommentEntity.TargetObject.song(song) = result.targetObject {
print(song)
}
//switch case let
switch result.targetObject {
case .song(let song):
print(song)
case .user(let user):
print(user)
}
方式 b.
改宣告栏位属性为 Base Class。
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
struct CommentEntity: Decodable {
enum CodingKeys: String, CodingKey {
case id
case comment
case targetObject = "target_object"
case commenter
}
enum PredictKey: String, CodingKey {
case type
}
var id: Int
var comment: String
var targetObject: Decodable
var commenter: UserEntity
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.comment = try container.decode(String.self, forKey: .comment)
self.commenter = try container.decode(UserEntity.self, forKey: .commenter)
//
let targetObjectContainer = try container.nestedContainer(keyedBy: PredictKey.self, forKey: .targetObject)
let targetObjectType = try targetObjectContainer.decode(String.self, forKey: .type)
if targetObjectType == "user" {
self.targetObject = try container.decode(UserEntity.self, forKey: .targetObject)
} else {
self.targetObject = try container.decode(SongEntity.self, forKey: .targetObject)
}
}
}
原理差不多,但这边先使用 nestedContainer
冲进去 targetObject
拿 type
出来判断,再决定 targetObject
要解析成什么类型。
要使用时再 Cast :
1
2
3
4
5
if let song = result.targetObject as? Song {
print(song)
} else if let user = result.targetObject as? User {
print(user)
}
场景2. 假设资料阵列栏位放多种类型的资料该如何 Decode?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"results": [
{
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
{
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "zhgchgli@gmail.com"
}
]
}
结合上述提到的 nestedUnkeyedContainer
+场景1. 的解决方案即可;这边也能改用 场景1. 的 a.解决方案 ,用 Associated Value Enum 存取值。
场景3. JSON String 栏位有给值时才 Decode
1
2
3
4
5
6
7
8
9
10
11
[
{
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
{
"type": "song",
"id": 11
}
]
使用 decodeIfPresent 进行 decode。
场景4. 阵列资料略过 Decode 失败错误的资料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"results": [
{
"type": "song",
"id": 99,
"name": "披星戴月的想你"
},
{
"error": "errro"
},
{
"type": "song",
"id": 19,
"name": "带我去找夜生活"
}
]
}
如前述,Decodable 预设是所有资料剖析都正确才能 Mapping 输出;有时会遇到后端给的资料不稳定,给一长串 Array 但就有几笔资料缺了栏位或栏位类型不符导致 Decode 失败;造成整包全部失败,直接 nil。
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
struct ResultsEntity: Decodable {
enum CodingKeys: String, CodingKey {
case results
}
var results: [SongEntity]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var nestedUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .results)
self.results = []
while !nestedUnkeyedContainer.isAtEnd {
if let song = try? nestedUnkeyedContainer.decode(SongEntity.self) {
self.results.append(song)
} else {
let _ = try nestedUnkeyedContainer.decode(EmptyEntity.self)
}
}
}
}
struct EmptyEntity: Decodable { }
struct SongEntity: Decodable {
var type: String
var id: Int
var name: String
}
let jsonString = "{ \"results\": [ { \"type\": \"song\", \"id\": 99, \"name\": \"披星戴月的想你\" }, { \"error\": \"errro\" }, { \"type\": \"song\", \"id\": 19, \"name\": \"带我去找夜生活\" } ] }"
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode(ResultsEntity.self, from: jsonString.data(using: .utf8)!)
print(result)
解决方式也类似 场景2.的解决方案 ; nestedUnkeyedContainer
遍历每个内容,并进行 try? Decode,如果 Decode 失败则使用 Empty Decode 让 nestedUnkeyedContainer
的内部指针继续执行。
*此方法有点 workaround,因我们无法对
nestedUnkeyedContainer
命令跳过,且nestedUnkeyedContainer
必须有成功 decode 才会继续执行;所以才这样做,看 swift 社群有人提增加 moveNext( ) ,但目前版本尚未实作。
场景5. 有的栏位是我程式内部要使用的,而非要 Decode
方式a. Entity/Model
这边就要提一开始说的,我们拆分 Entity/Model 的功用了;Entity 单纯负责 JSON String to Entity(Decodable) Mapping;Model initWith Entity,实际程式传递、操作、商业逻辑都是使用 Model。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct SongEntity: Decodable {
var type: String
var id: Int
var name: String
}
class SongModel: NSObject {
init(_ entity: SongEntity) {
self.type = entity.type
self.id = entity.id
self.name = entity.name
}
var type: String
var id: Int
var name: String
var isSave:Bool = false //business logic
}
拆分 Entity/Model 的好处:
权责分明,Entity: JSON String to Decodable, Model: business logic
一目了然 mapping 了哪些栏位看 Entity 就知道
避免栏位一多全喇在一起
Objective-C 也可用 (因 Model 只是 NSObject、struct/Decodable Objective-C 不可见)
内部要使用的商业逻辑、栏位放在 Model 即可
方式b. init 处理
列出 CodingKeys 并排除内部使用的栏位,init 时给预设值或栏位有给预设值或设为 Optional,但都不是好方法,只是可以 run 而已。
[2020/06/26 更新] — 下篇 场景6.API Response 使用 0/1 代表 Bool,该如何 Decode?
[2020/06/26 更新] — 下篇 场景7.不想要每每都要重写 init decoder
[2020/06/26 更新] — 下篇 场景8.合理的处理 Response Null 栏位资料
综合场景范例
综合以上基本使用及进阶使用的完整范例:
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
{
"count": 5,
"offset": 0,
"limit": 10,
"results": [
{
"id": 123456,
"comment": "是告五人,不是五告人!",
"target_object": {
"type": "song",
"id": 99,
"name": "披星戴月的想你",
"create_date": "2020-06-13T15:21:42+0800"
},
"commenter": {
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "zhgchgli@gmail.com",
"birthday": "1994/07/18"
}
},
{
"error": "not found"
},
{
"error": "not found"
},
{
"id": 2,
"comment": "哈哈,我也是!",
"target_object": {
"type": "user",
"id": 1,
"name": "zhgchgli",
"email": "zhgchgli@gmail.com",
"birthday": "1994/07/18"
},
"commenter": {
"type": "user",
"id": 1,
"name": "路人甲",
"email": "man@gmail.com",
"birthday": "2000/01/12"
}
}
]
}
Output:
1
zhgchgli:是告五人,不是五告人!
完整范例演示如上!
(下)篇&其他场景已更新:
总结
选择使用 Codable 的好处,第一当然是因为原生,不用怕后续无人维护、还有写起来漂亮;但相对的限制较严格、比较不能灵活解 JSON String,不然就是要如本文做更多的事去完成、还有效能其实不比使用其他 Mapping 套件优(Decodable 依然使用Objective 时代的 NSJSONSerialization 进行解析),但我想在后续的更新中或许苹果会对此进行优化,那时我们也不必更动程式。
文中场景、范例或许有些很极端,但有时候遇到了也没办法;当然希望一般情况下单纯的 Codable 就能满足我们的需求;但有了以上招式之后应该没有打不倒的问题了!
感谢 @saiday 大大技术支援。
延伸阅读
深入 Decodable — — 写一个超越原生的 JSON 解析器 满满的内容,深入了解 Decoder/JSONDecoder。
Why Model Objects Shouldn’t Implement Swift’s Decodable or Encodable Protocols
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。