相对于冗长的XML, JSON格式的文件现在应用得愈加广泛。JSON虽然看似简单,但有些小陷阱须得注意。再加上解析时,必须双向考虑JSON文件与代码之间的衔接,这使得在iOS中解析JSON具有一定的难度。本文通过使用Swift语言,分4个步骤,从最简单的例子开始,最后讨论解决较为棘手的数组问题,在编码过程中具有一定的意义。
## 最简单的JSON解析
设有一个名为*test.json*的文件,其内容如下:
{
"name": "Mike"
}
又有一个名为Person的struct:
struct Person: Decodable {
var name: String
}
则可如下解析:
var url = Bundle.main.url(forResource: "test", withExtension: "json")
do {
let data = try Data(contentsOf: url!)
let decoder = JSONDecoder()
let person = try decoder.decode(Person.self, from: data)
print(person.name)
} catch {
print("error: \(error.localizedDescription)")
}
则打印出:
Mike
上面须满足这几个条件:
1. Person必须具备Decodable的protocol;
2. 因为*test.json*有一个*name*的属性,相对应地,Person也须有一个名为*name*的变量。
此外,*test.json*必须是一个有效的JSON格式。比较容易犯的错误是:
{
name: "Mike"
}
由于没有将*name*以双引号括起来,导致无效的JSON格式,解析时就会出错。
*decode<T>(T.Type, from: Data)*方法返回一个类型的实例,在上面的例子中是*Person*的实例。
小结: **凡是在JSON中出现"{}"的地方,应在代码中为其设立一个类或结构。同时将JSON中的键设为类或结构的变量。**
## 解析多个属性
为*test.json*增加一个*sex*的属性。
{
"name": "Mike",
"sex": "male"
}
则Person也应增加一个*sex*的变量。
struct Person: Decodable {
var name: String
var sex: String
}
注意,在*test.json*中,多个并列的属性,须以逗号","隔开,否则要出错。
## 解析嵌套的属性
下面增加电话属性。因为可能拥有多个电话,因此,有必要分别增加*home*及*cellPhone*的属性。
{
"name": "Mike",
"sex": "male",
"phones": {
"home": "66308273",
"cellPhone": "13907567320"
}
}
*home*及*cellPhone*是*phones*的嵌套属性。在代码中,可以将*phones*当作一个类或结构,因此,与前相似,需为其新定义一个结构。
struct Phones: Decodable {
var home: String
var cellPhone: String
}
有了这个结构,Swift就能自动将嵌套的属性对应到*Phones*结构中。
当然,应为*Person*加上*phones*这个新的变量:
struct Person: Decodable {
var name: String
var sex: String
var phones: Phones
}
## 解析数组
上面已经成功地解析了一个Person. 下面看看如何同时解析多个Persons.
先修改*test.json*文件。
[
"name": "Mike",
"sex": "male",
"phones": {
"home": "66308273",
"cellPhone": "13907567320"
},
"name": "Alice",
"sex": "female",
"phones": {
"home": "68557026",
"cellPhone": "13693528325"
}
]
首先,最外层的符号改为中括号,以表示数组,且第一个人与第二个人的信息之间用“,”相隔开。
这样的JSON格式对吗?
不对,虽然使用了中括号表示数组,虽然两个人之间以一个空行隔开,但在JSON中,这不是两个对象的组合,而是具有重复键的无效格式。让我们简化一下,只保留*name*的属性来看看。
[
"name": "Mike",
"name": "Alice"
]
这是两个具有相同键的组合,而不是两个对象的组合。对于对象的组合,每个对象均应以"{}"包围起来。
[
{"name": "Mike"},
{"name": "Alice"}
]
推而广之,完整的JSON似乎应是这样:
[
{
"name": "Mike",
"sex": "male",
"phones": {
"home": "66308273",
"cellPhone": "13907567320"
}
},
{
"name": "Alice",
"sex": "female",
"phones": {
"home": "68557026",
"cellPhone": "13693528325"
}
}
]
这就对了吗?array的原型是:
[value, value, value, ...]
而object是value的一种。object的原型是:
{string: value}
即,array必须符合object的要求,而object要求必须具有键值对。也就是,先有键,再有用array来表示的值系列。
因此,正确的应是:
{
"persons": [
{
"name": "Mike",
"sex": "male",
"phones": {
"home": "66308273",
"cellPhone": "13907567320"
}
},
{
"name": "Alice",
"sex": "female",
"phones": {
"home": "66308273",
"cellPhone": "13907567320"
}
}
]
}
一句话,数组的前面应该有键,且最外层应以"{}"包围。
现在,看着这个JSON结构,最外层应以"{}"包围,说明代码应为其定义一个新类或新的结构,且拥有一个*persons*的变量。
struct PersonArray: Decodable {
var persons: [Person]
}
而解析的则可改成:
do {
let data = try Data(contentsOf: url!)
let decoder = JSONDecoder()
let personArray = try decoder.decode(PersonArray.self, from: data)
for person in personArray.persons {
print("\(person.name)")
}
} catch {
print("error: \(error.localizedDescription)")
}
实际上,上面的*PersonArray*这个结构名取得不好,导致*personArray.persons*看得很别扭,如同*shoppingCar.shoppingItems*,最好将其重命名为一个表示特定范围的更加具体的集合名称,如Americans.
这就变成:
struct Americans: Decodable {
var persons: [Person]
}
do {
...
let americanPersons = try decoder.decode(Americans.self, from: data)
for person in americanPersons {
print("\(person.name)")
}
}
...
## 结语
有了这个例子,就很容易将其复制、修改、扩充至其他领域了。
## 练习题
使用JSON表示一个包含乾、兑、离、震、巽、坎、艮、坤的八卦,并将其进行解析。
## 参考资料
1. 介绍JSON, json.org, http://www.json.org/json-zh.html
2. iOS Apprentice, Sixth Edition, Fahim Farook & Matthijs Hollemans, Chapter 34, Sect 3: Parse JSON