用Codable协议实现快速 JSON 解析

Codable

解释就是:可编码

在 Swift 4 中新推出的 Codable 从根本上改进了使用 JSON 和其他数据表示方式的工作方式。
Codable 提供了将 JSON 映射到 Swift 模型的简洁方法。
得益于编译器的自动代码整合,
Codable 协议的使用如果你是自定义也要遵循,
Swift 系统库中的 String,Int,Double,Date,URL,Data 等

你:“真的有那么神奇吗?”
我:”是的“。

例如:
创建一个 Landmark 结构:(存储地标的名称和创始年份)

struct Landmark {
var name: String
var foundingYear: Int
}

整合 Codable:(从Encodable和Decodable添加Codable到继承列表Landmark 自动处理满足所有的协议要求)

struct Landmark: Codable {
 var name: String
var foundingYear: Int
}

无需额外代码,JSON 的编码与解码就自动完成了。

Swift标准库定义了数据编码和解码的标准化方法。

你可以通过在自定义类型上实现EncodableDecodable协议来采用此方法。
采用这些协议允许EncoderDecoder协议的实现接收你的数据,并将其编码或解码到外部表示(如JSON或属性列表)或从外部表示解码。为了支持编码和解码两者中,声明符合Codable,它结合了EncodableDecodable协议。这个过程被称为使你的类型可编码

好了先看一下它的定义吧:

Codable 协议的定义:
typealias Codable = Decodable & Encodable

它是 Protocol 的集合,也就是:

Decodable 【作数据解析】和
Encodable【用作数据编码】。

声明一个实体类 Person 它声明实现了 Codable:

struct Person : Codable {
// 简单的几个属性声明
var name: String
var gender: String
var age: Int
}

把它的实例编码成 JSON 字符串:

let person = Person(name: "swift", gender: "male", age: 24)

// 数据编码
let encoder = JSONEncoder()
let data = try! encoder.encode(person)
let encodedString = String(data: data, encoding: .utf8)!
print(encodedString) // 输出 json字符串:{"name":"swift","age":24,"gender":"male"}

JSON 编码操作,是不是很简单:
1、初始化了一个 Person 实例。
2、初始化了一个 JSONEncoder。
3、调用它的 encode 方法,把 person 实例进行编码。

上面的数据编码可以理解为服务器的返回json,如下:

{
"name":"swift",
"age":24,
"gender":"male"
},
{
"name":"Object-C",
 "age":26,
"gender":
"male"
}

当我们拿到服务器返回的json时,应该怎么解析呢?

let jsonString = "{"name":"swift","age":22,"gender":"female"}"
let jsonData = jsonString.data(using: .utf8)!
// 据解析
let decoder = JSONDecoder()

//据解析
let result = try! decoder.decode(Person.self, from: jsonData)
print(result)  // 输出: Person(name: "swift", gender: "female", age: 22)

注意:
1、给它的 decode 方法传入要解析的实例类型 Person.self
2、添加解析数据对象 jsonData, 就完成了 JSON 数据的解析了。

总结一下:
只需要你的实体类属性名和 JSON 数据能够对应上,就可以实现内容的解析。 
Codable自定义: 在属性里添加结构体
// 给下面的 var location: 使用
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
}

struct Landmark: Codable {
// Double, String, and Int 一切都符合Codable的定义
var name: String
var foundingYear: Int

// 添加自定义Codable类型的属性,保持整体Codable一致性。
var location: Coordinate
}

从上面可以看出添加 结构体 是可以的,那么数组呢?也可以。
好吧,接下来看复杂一点的数据结构:

struct Landmark: Codable {
// 常规的属性
var name: String
var foundingYear: Int
var location: Coordinate

// 添加这些属性后,Landmark仍然可编码.
var vantagePoints: [Coordinate]
var metadata: [String: String]
var website: URL?
}

单个 编码(Encode) 或解码(Decode)

在某些情况下,你可能不需要Codable支持双向编码和解码。
例如,某些应用程序只需要调用远程网络API,而不需要解码包含相同类型的响应。
Encodable如果你只需要支持数据编码Encode,则声明符合性。相反,Decodable如果你只需要读取给定类型的数据,则声明符合性。

下面的示例显示Landmark,仅对数据进行编码或解码的结构的替代声明:

//可编码
struct Landmark: Encodable {
var name: String
var foundingYear: Int
}

// 解码
struct Landmark: Decodable {
var name: String
var foundingYear: Int
}
编码 plist 数据格式

PropertyListEncoder

使用CodingKeys CodingKey对指定属性进行编码

默认情况下,如果声明继承了 Codable 协议,这个实例中的所有属性都会被算作编码范围内。

问题一 ?

如果你只想对一部分属性进行编解码,怎么办呢?

答:你可以在自定义类中声明一个 CodingKeys 枚举属性:
struct Landmark: Codable {
var name: String
var foundingYear: Int
var location: Coordinate
var vantagePoints: [Coordinate]

// 声明一个 CodingKeys 枚举属性
enum CodingKeys: String, CodingKey {

  // 1、只有在 CodingKeys 中指定的属性名才会进行编码
  //  2、这时的 name 会变为 ‘title’
    case name = "title"
    case foundingYear = "founding_date"
    
    case location
    case vantagePoints
  }
}

注意:
那些没有在 CodingKeys 中声明的属性就必须要要有一个默认值。

问题二 ?

在开发中有时会遇到:序列化数据格式中使用的键与数据类型中的属性名称不匹配。怎么办?

答:通过指定枚举String的原始值类型来处理。

例如:
使用 CodingKeys 改变编码属性的名称(看上面的代码)

之前的解析结果是:
{"name":"xxx","foundingYear":xxx, location:xxx, vantagePoints:xxx}
最后的编码结果就是:
{"title":"xxx","founding_date":xxx,location:xxx, vantagePoints:xxx}

(自定义)手动编码和解码

例如:给 Coordinate 添加一个属性 elevation,注意这里使用到了嵌套 层级关系,注意看 struct Coordinate { } 的括号范围。

// 定义 Coordinate (坐标)
struct Coordinate {
var latitude: Double
var longitude: Double
// 海拔
var elevation: Double

enum CodingKeys: String, CodingKey {
    case latitude
    case longitude
    case additionalInfo
}
// 添加一个 additionalInfo 属性,该属性下可以有许多其它属性(添加海拔详细信息)
enum AdditionalInfoKeys: String, CodingKey {
    case elevation
  }
}

这样的 结构是:

{
"latitude" : xxx
"longitude": xxx
"additionalInfo": {
    "elevation" : xxx
    "elevation2": xxx
  }
}

如果不给 elevation 一个 enum 的话,结构是:

struct Coordinate {
var latitude: Double
var longitude: Double
var elevation: Double

enum CodingKeys: String, CodingKey {
case latitude
case longitude
case additionalInfo
case elevation
  }
 }

解析的结果就是:(没有嵌套)

{
"latitude" : xxx
"longitude": xxx
"additionalInfo" : xxx
"elevation": xxx
}

明白为啥添加多一个枚举之后,接着,回到主题。
我们还需要手动的确立他们之间的对应关系:
来个套路:首先建一个extension

// init(from:)

extension Coordinate: Decodable {
init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    latitude = try values.decode(Double.self, forKey: .latitude)
    longitude = try values.decode(Double.self, forKey: .longitude)
    
    let additionalInfo = try values.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
    elevation = try additionalInfo.decode(Double.self, forKey: .elevation)
  }
}

 // encode(to:)
extension Coordinate: Encodable {
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(latitude, forKey: .latitude)
    try container.encode(longitude, forKey: .longitude)
    
    var additionalInfo = container.nestedContainer(keyedBy: AdditionalInfoKeys.self, forKey: .additionalInfo)
    try additionalInfo.encode(elevation, forKey: .elevation)
}

}

上面的疑惑?

  • init(from decoder: Decoder) 用于解析数据。
  • encode(to encoder: Encoder) 方法用于编码数据。
  • decoder.container() 方法首先获取 CodingKey 的对应关系。
  • CodingKeys.self 表示我们先前声明的类型。
  • 用 values.decode() 方法解析单个属性。
  • values.nestedContainer() 方法获取内嵌的层级
  • 该encode(to:)方法的该实现反转了前一示例的解码操作。

如果解析成功:

{
    "latitude":"xxx",
    "longitude":xxx,
    "additionalInfo":{
                     "elevation":"xxx",
                     "elevation2":"xxx"
                }
}

end

数据解析,实际开发中是经常使用到的,大家务必掌握。

最后给个API 给大家练习

https://api.douban.com//v2/movie/in_theaters

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容