用 Codable 协议实现快速 JSON 解析

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

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

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 项目,它是一个你值得了解的特性。

转自:http://swiftcafe.io/post/codable

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