Post

Swift Codable 解码问题全攻略|多场景 JSON 解析与错误容忍技巧

针对 iOS 开发中 Swift Codable 解码常见困境,提供多场景 JSON 解析实战范例,解决栏位多型、阵列错误容忍与自订日期格式等问题,助你打造稳健高效的资料映射流程。

Swift Codable 解码问题全攻略|多场景 JSON 解析与错误容忍技巧

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


现实使用 Codable 上遇到的 Decode 问题场景总汇(上)

从基础到进阶,深入使用 Decodable 满足所有可能会遇到的问题场景

Photo by [Gustas Brazaitis](https://unsplash.com/@gustasbrazaitis){:target="_blank"}

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:

设定 JSONDecoderdateDecodingStrategy

  • .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 常识:

  1. Decodable Protocol 内的的栏位类型(struct/class/enum),都须实作 Decodable Protocol;亦或是在 init decoder 时赋予值

  2. 栏位类型不相符时会 Decode 失败

  3. Decodable Object 中栏位设为 Optional 的话则为可有可无,有给就 Decode

  4. Optional 栏位可接受: JSON String 无栏位、有给但给 nil

  5. 空白、0 不等于 nil,nil 是 nil;弱型别的后端 API 需注意!

  6. 预设 Decodable Object 中有列举且非 Optional 的栏位,若 JSON String 没给会 Decode 失败(后续会说明如何处理)

  7. 预设 遇到 Decode 失败会直接中断跳出,无法单纯跳过有误的资料(后续会说明如何处理)

[左:”” 右:nil](https://josjong.com/2017/10/16/null-vs-empty-strings-why-oracle-was-right-and-apple-is-not/){:target="_blank"}

左:”” / 右:nil

进阶使用

到此为止基本的使用已经完成了,但现实世界不会那么简单;以下列举几个进阶会遇到的场景并提出适用 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 栏位可能的对象是 UserSong ? 那该如何处理?

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 冲进去 targetObjecttype 出来判断,再决定 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 的好处:

  1. 权责分明,Entity: JSON String to Decodable, Model: business logic

  2. 一目了然 mapping 了哪些栏位看 Entity 就知道

  3. 避免栏位一多全喇在一起

  4. Objective-C 也可用 (因 Model 只是 NSObject、struct/Decodable Objective-C 不可见)

  5. 内部要使用的商业逻辑、栏位放在 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 大大技术支援。

延伸阅读

  1. 深入 Decodable — — 写一个超越原生的 JSON 解析器 满满的内容,深入了解 Decoder/JSONDecoder。

  2. 不同角度看问题 — 从 Codable 到 Swift 元编程

  3. Why Model Objects Shouldn’t Implement Swift’s Decodable or Encodable Protocols

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.