本文原载于我的博客:www.seekingmini.top/archives/sw…
0 写在前面
平时写小项目时请求JSON格式的数据,怎么解析向来是一个大问题,尤其碰到嵌套型的数据,更是不知道怎么写代码才好,所以写一篇教程来记录一下。刚好好久也没写博客了,赶快写一篇来刷刷存在感...
1 JSON是什么
JSON是一个以文字为主、轻量型,并且容易使用来储存以及交换资料的方式。它通常用来呈现结构性的数据,许多我每天使用的网页服务都是以JSON格式为主的API。
一句话概括,一个JSON对象
,就是一个个的<key, value>
对的集合({<key, value>}
)。
- 举例
{
"name": "SeekingMini",
"age": 21,
"hobby": ["Swimming", "Coding"],
"location": {
"country": "China",
"city": "Suzhou"
}
}
在这个例子中,最外层的{}
包含了4个key
,分别是name
、age
、hobby
、location
,它们都是String
类型;而它们对应value
的类型分别是String
、Int
、[String]
、JSON对象
。我们发现,key
的类型一定是String
即字符串,而value
的类型是任意的。
正是因为value
的类型可以是任何类型,如果value
是JSON对象
,这意味着出现了嵌套,如location
对应的value
就是一个嵌套在JSON对象
里的JSON对象
,这为我们解析JSON数据增加了复杂度。上面的例子可以用下面的结构图表示:
|-- TOP
|-- "name" -> "SeekingMini"
|-- "age" -> 21
|-- "hobby" -> ["Swimming", "Coding"]
|-- "location"
|-- "country" -> "China"
|-- "city" -> "Suzhou"
把它旋转90度来看,其实就是一个N叉树。根结点忽略不计,其他的非叶子结点都是key
,而叶子结点都是value
。当然了,如果没有嵌套,那么这棵树只有3层,顶层是TOP
,表示最外层的大括号;第2层是key
;第3层是value
。
但是出现了嵌套,location
的子树就是它的value
,而这棵子树自身是一个JSON
对象,它的叶子结点就是非JSON
对象类型的value
。
还是不清楚什么是JSON的话,就参考菜鸟教程吧!我们的重头戏放在如何解析包含嵌套的JSON数据。
2 解析JSON数据
解析一个JSON格式的数据,最重要的是什么?我认为应该是结构层次。我们只有定义了准确的结构层次,才能在每一层解析出正确的数据。
1) 无脑struct
叠加
还是以上面的数据为例,我们可以定义出这样的结构体:
struct Person: Codable {
var name: String
var age: Int
var hobby: [String]
var location: Location
}
extension Person {
struct Location: Codable {
var country: String
var city: String
}
}
稍微说明一下:Person
这个结构体就代表了整个JSON对象
的结构。而Person
中的每个属性代表key
;属性值代表value
。由于存在嵌套,所以location
这个key
的value
需要重新定义一个结构体Location
。这样的话就能解析了。解析代码如下:
// json data
let jsonData = """
{
"name": "SeekingMini",
"age": 21,
"hobby": ["Swimming", "Coding"],
"location": {
"country": "China",
"city": "Suzhou"
}
}
"""
// parse
if let data = jsonData.data(using: .utf8) {
let decoder = JSONDecoder()
if let person = try? decoder.decode(Person.self, from: data) {
print(person)
print(person.name)
print(person.age)
print(person.hobby)
print(person.location.country)
print(person.location.city)
}
}
// 打印结果如下:
// Person(name: "SeekingMini", age: 21, hobby: ["Swimming", "Coding"], location: // __lldb_expr_45.Person.Location(country: "China", city: "Suzhou"))
// SeekingMini
// 21
// ["Swimming", "Coding"]
// China
// Suzhou
struct
的叠加非常简单粗暴,但是会有问题,比如说我的结构体定义成这样:
struct Person: Codable {
var name: String
var age: Int
var hobby: [String]
// 比如说我不定义嵌套的Location结构体,而直接在外层定义country和city
var country: String
var city: String
}
那么location
这个key是解析不出来的,更别提country
和city
了。我们应该怎么办呢?
2) 使用enum
和CodingKey
协议
利用enum
定义JSON对象的结构:
enum CodingKeys: String, CodingKey {
// 最外层的key(把需要用到的定义出来即可)
case name
case age
case hobby
case location
enum LocationKeys: String, CodingKey {
// location层对应的key
case country
case city
}
}
name
、age
、hobby
和location
这4个key
在同一层,而country
、city
在下一层。每一层就是一个enum
。通过这样的定义,我们可以写出以下的代码来解析:
init(from decoder: Decoder) throws {
// 处理name、age和hobby的解码
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
name = try rootContainer.decode(String.self, forKey: .name)
age = try rootContainer.decode(Int.self, forKey: .age)
hobby = try rootContainer.decode([String].self, forKey: .hobby)
// 处理country和city的解码
let locationContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.LocationKeys.self, forKey: .location)
country = try locationContainer.decode(String.self, forKey: .country)
city = try locationContainer.decode(String.self, forKey: .city)
}
从decoder
使用 container(keyedBy:)
获取顶级容器后, 我们可以重复使用这些方法:
nestedContainer(keyedBy:forKey:)
:从给定键的对象获取嵌套对象nestedUnkeyedContainer(forKey:)
:从给定键的对象获取嵌套数组nestedContainer(keyedBy:)
:从数组中获取下一个嵌套对象nestedUnkeyedContainer()
:从数组中获取下一个嵌套数组
完整代码如下:
import Foundation
// json data
let jsonData = """
{
"name": "SeekingMini",
"age": 21,
"hobby": ["Swimming", "Coding"],
"location": {
"country": "China",
"city": "Suzhou"
}
}
"""
struct Person {
var name: String
var age: Int
var hobby: [String]
var country: String
var city: String
}
extension Person: Decodable {
enum CodingKeys: String, CodingKey {
// 最外层的key
case name
case age
case hobby
case location
enum LocationKeys: String, CodingKey {
// location层对应的key
case country
case city
}
}
init(from decoder: Decoder) throws {
// 处理name、age和hobby的解码
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
name = try rootContainer.decode(String.self, forKey: .name)
age = try rootContainer.decode(Int.self, forKey: .age)
hobby = try rootContainer.decode([String].self, forKey: .hobby)
// 处理country和city的解码
let locationContainer = try rootContainer.nestedContainer(keyedBy: CodingKeys.LocationKeys.self, forKey: .location)
country = try locationContainer.decode(String.self, forKey: .country)
city = try locationContainer.decode(String.self, forKey: .city)
}
}
// parse
if let data = jsonData.data(using: .utf8) {
let decoder = JSONDecoder()
if let person = try? decoder.decode(Person.self, from: data) {
print(person)
}
}
// 打印结果
// Person(name: "SeekingMini", age: 21, hobby: ["Swimming", "Coding"], country: "China", city: "Suzhou")
使用enum
和CodingKey
协议有一个好处:对于Struct
的定义不用考虑JSON对象
的结构,而JSON对象
的结构都是通过enum
的嵌套来定义的。
参考
如何使用Swift Decodable协议解码嵌套的JSON结构?
《iOS 13 App程式設計進階攻略》