用 Codable 协议实现快速 JSON 解析

swift 发布于 2019年06月24日

如果你是一名有一定开发经验的开发者,那么你就一定会遇到过数据解析的问题。 最常见的就是 JSON 数据的解析,你的 APP 总会要请求一些服务器数据,比如各种信息列表,配置数据等。

如果你之前用过 Objective-C 的话, 那么你一定对 NSJSONSerialization 并不陌生。 它的总体步骤大致是这样,先从 Data 对象中解析出 NSDictionaryNSArray, 然后在从这里面按照属性名称取出需要的值,最后再用这些值给实体对象赋值。

总体来说这个操作并不轻松,而且比较容易出差错,比如你在写解析代码的时候把属性名写错了,或者某个 nil 判断没有处理正确,导致了程序意外崩溃,就需要花不少时间进行调试。

Codable

我们的主题自然不是 NSJSONSerialization, 而是 Swift 中提供的 Codable 协议。 它和前者有着相似的作用,但应用范围更广,并且易用性更好。 先来看一下 Codable 协议的定义:

typealias Codable = Decodable & Encodable

它其实另外两个 Protocol 的集合,也就是 DecodableEncodable。 一个用作数据解析,另一个用作数据编码。 其他不多说,咱们先来看一个实例,我们先声明一个实体类 Person 它声明实现了 Codable

struct Person : Codable {

var name: String
var gender: String
var age: Int

}

除了声明 Codable 之外,这个实体类并没有其他代码,只有几个属性声明。 如果我们需要把他的实例编码成 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) // 输出 {"name":"swift","age":24,"gender":"male"}

如上所示,首先初始化了一个 Person 实例。 然后初始化了一个 JSONEncoder。 再调用它的 encode 方法,把 person 实例进行编码。 让后整个 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)

解析的时候用的是 JSONDecoder 对象,给他的 decode 方法传入要解析的实例类型 - Person.self, ,再加上要解析的数据对象 jsonData 就完成了 JSON 数据的解析。

使用 Codable 协议就是这么简单, 你不需要些任何具体的解析代码,只需要你的实体类属性名和 JSON 数据能够对应上,就完成了内容的解析。 这样相比 NSJSONSerialization 来看,精简了很多,并且不容易出错。

这里只有一点需要注意,对于我们刚才例子中的 Person 类,除了它自己实现 Codable 协议之外,它的所有属性也必须是遵循 Codable 的。 Swift 系统库中的 String,Int,Double,Date,URL,Data 这些类都是实现了 Codable 的。 如果你的自定义属性是其他类型,则需要注意一下它是否也实现了 Codable

另外, 除了 JSONEncoderJSONDecoder 之外, Swift 还为其他类型的数据提供了编解码能力, 比如 PropertyListEncoder 可以编码 plist 数据格式。

对指定属性编码

默认情况下,如果声明继承了 Codable 协议,这个实例中的所有属性都会被算作编码范围内。 如果你只想对一部分属性进行编解码,也是有办法的,可以在你的自定义类中声明一个 CodingKeys 枚举属性:

struct Person : Codable {

var name: String
var gender: String = ""
var age: Int

enum CodingKeys: String, CodingKey {

case name
case age

}

}

还是之前的 Person 类,这次我们加入了 CodingKeys 属性,并且定义了两个枚举值 nameage,只有在 CodingKeys 中指定的属性名才会进行编码,如果我们再次对 Person 进行编码,得到的将会是这样的结果:

{"name":"swift","age":24}

可以看到, gender 属性由于没有在 CodingKeys 中声明,所以不会被编码。 另外如果使用了 CodingKeys,那些没有在 CodingKeys 中声明的属性就必须要要有一个默认值,我们上面的代码中其实给 gender 属性也声明了默认值。

我们还可以使用 CodingKeys 改变编码属性的名称:

struct Person : Codable {

var name: String
var gender: String = ""
var age: Int

enum CodingKeys: String, CodingKey {

case name = "title"
case age

}

}

还是以 Person 为例,这次我们在 CodingKeys 枚举中讲 name 属性重新定义为 title。 这个意思就是说,虽然在 Person 类中,这个属性名还是 name, 但在编码后的 JSON 中,它的属性名就应该是 title

对上面这个类运行编码后,得到的结果是这样:

{"title":"swift","age":24}

JSON 中的第一个属性名变成了 title, 它对应 Person 类中的 name 属性。

自定义编码过程

你还可以自定义整个编码和解码过程。 对于稍复杂一些的数据结构,这个能力还是会经常用到的。 比如我们想给 Person 再加上身高和体重两个属性:

struct Person : Codable {

var name: String
var gender: String = ""
var age: Int

var height: Int
var weight: Int

enum CodingKeys: String, CodingKey {

case name = "title"
case age
case body

}

enum BodyKeys: String, CodingKey {

case height
case weight

}

}

这里面新增的 heightwidth 属性,分别对应体重和身高。 并且还增加了另外一个属性 BodyKeys。 为什么要添加这个属性呢? 是因为我们这次准备把 heightwidth 放到一个单独的对象中。 下面这样解释可能会更直观一些,如果我们不添加 BodyKeys 属性,而是把他们直接定义到 CodingKeys 里面,那么生成的 JSON 结构大致是这样:

{
"name" : xxx
"age": xxx
"height" : xxx
"weight": xxx
}

但我们单独为 heightweight 定义了 BodyKeys 枚举属性。 并且把它有声明到了 CodingKeys 中。 这次 CodingKeys 多了一个 body 属性,它对应的就是 BodyKeys 这个枚举。 至于这个对应关系怎么确立的,稍后会讲到。

{
"name" : xxx
"age": xxx
"body": {
"height" : xxx
"weight": xxx
}
}

这样我想应该就说明了 BodyKeys 的作用了。 这样声明完还不行,我们还需要手动的确立他们之间的对应关系,这就要重载 Codable 的两个方法:

extension Person {

init(from decoder: Decoder) throws {

let vals = try decoder.container(keyedBy: CodingKeys.self)
name = try vals.decode(String.self, forKey: CodingKeys.name)
age = try vals.decode(Int.self, forKey: CodingKeys.age)

let body = try vals.nestedContainer(keyedBy: BodyKeys.self, forKey: .body)
height = try body.decode(Int.self, forKey: .height)
weight = try body.decode(Int.self, forKey: .weight)

}

func encode(to encoder: Encoder) throws {

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)

var body = container.nestedContainer(keyedBy: BodyKeys.self, forKey: .body)
try body.encode(height, forKey: .height)
try body.encode(weight, forKey: .weight)

}

}

init(from decoder: Decoder) 用于解析数据, encode(to encoder: Encoder) 方法用于编码数据。 上面的代码我想不用过多解释,很容易理解。

decoder.container() 方法首先获取 CodingKey 的对应关系,这里我们首先传入 CodingKeys.self 表示我们先前声明的类型。 然后调用 vals.decode() 方法,用于解析某个单独的属性。 接下来调用 vals.nestedContainer() 方法获取内嵌的层级,也就是我们先前声明的 BodyKeys。然后继续解析。

编码的相关处理也大同小异,把上面解码方法中的逻辑反向处理了一遍。

这样,如果我们对新的 Person 实例再进行编码,得到的将会是这样的结果:

{"title":"swift","age":24,"body":{"weight":80,"height":180}}

可以看到,生成了带层级的 JSON 数据。

总结

Codable 协议的设计,可以帮助我们产出更好的代码结构。对于简单的数据模型,不需要任何处理即可使用。 而稍复杂的数据结构,也只需要将解析规则封装到实体类中,可以有效避免代码结构的散乱。

总之,像是数据解析这类的操作,在平时的开发工作中还是比较多的。 如果你正在开发 Swift 项目,它是一个你值得了解的特性。


如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
关注微信公众号
发现更多精彩
swift-cafe