在Swift中使用JSON

第一次尝试译文,如果有错误谢谢指正。这里面的例子有的是我自己通过XCTest测试的. KennyBest!

译文地址:https://developer.apple.com/swift/blog/?id=37

如果你的APP和web程序进行交互,那么从服务器返回的数据大都是JSON格式。你可以使用Foundation框架中的JSONSerialization类将JSON数据转化为Swift类型数据,例如DictionaryArrayStringNumber、Bool。然而,你并不能确定从服务器获取到的JSON值或者结构,正确地转化模型对象变成难题。这篇博文正是描述一些当你使用JSON交互时可采用的途径。

<! – more –>

从JSON中取数据

JSONSerializtion类提供了jsonObject(with:options:)类方法返回数据类型为Any数据,如果当数据不能被解析时抛出一个错误。

1
2
3
4
5
6
7
8
9
func testExample() {
let dictionary: [String : String] = [ "name" : "lilingjie" ]
let data: Data = try! JSONSerialization.data(withJSONObject: dictionary, options: .prettyPrinted)
let retDictionary = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String : String]
XCTAssertEqual(dictionary, retDictionary!, "JSONSerialization convert JSON to Swift data type")
}

尽管有效的JSON可能只包含单个数据,从服务器响应的数据通常以数组或者字典作为最外层模型进行编码。你可以在ifguard语法中使用可选绑定和as?as!类型指定操作符以常量的形式取出来已知类型的数据。通常指定为[String : Any]来从JSON数据中获取字典Dictionary,指定为[Any]获得数组Array(或者更明确元素类型的数组,像[String])。 借助下标语法存取Subscript和枚举元素类型可选绑定后你可以通过key值摘取字典数据或者通过下标取得数组数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func testJSONSerializtionToDictionary() {
/**
以字典作为最外层的JSON数据
{
"someKey": 42.0
"anotherKey": {
"someNestedKey": true
}
}
*/
let jsonString = "{ \"someKey\":42.0 \"anotherKey\": {\"someNestedKey\": true}}"
let data = jsonString.data(using: .utf8)
XCTAssertNotNil(data, "JSONSerialization Data 序列化失败")
let jsonWithObjectRoot = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments)
if let dictionary = jsonWithObjectRoot as? [String : Any] {
XCTAssertNotNil(dictionary, "convert OK")
XCTAssertEqual(dictionary["someKey"] as? Double, 42.0 , "two value not equal")
}
}

Swiftbuilt-in语言特性使得不需要导入额外的库或框架,就可以轻松安全地摘取和使用由Foundation框架中的API解码的JSON数据。

利用从JSON获取的数据创建Model

大部分APP遵循MVC设计模式,在一个类的定义中将JSON数据转化为对象变得很平常。
例如,当你写一个提供当地餐馆搜索结果的APP时,你可能实现一个Restaurant模型,在这个模型中定义一个接受JSON数据的构造函数和一个请求餐馆列表的类方法。
从下面这个类中考虑

1
2
3
4
5
6
7
8
9
10
11
12
13
import Foundation
struct Restaurant {
enum Meal: String {
case breakfast, lunch, dinner
}
let name: String
let coordinates: (latitude: Double, longitude: Double)
let meals: Set<Meal>
}

一个餐馆有String类型name属性,元组类型coordinates属性以及Set类型Meal属性。
服务器响应数据格式:

1
2
3
4
5
6
7
8
9
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
}

写一个可选的JSON构造函数

为了从JSON数据转化为一个餐馆对象,写一个带有Any类型参数的构造函数,旨在从JSON数据中拿取数据且转变为属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
extension Restaurant {
init?(json: [String: Any]) {
guard let name = json["name"] as? String,
let coordinatesJSON = json["coordinates"] as? [String: Double],
let latitude = coordinatesJSON["lat"],
let longitude = coordinatesJSON["lng"],
let mealsJSON = json["meals"] as? [String] else {
return nil
}
var meals: Set<Meal> = []
for string in mealsJSON {
guard let meal = Meal(rawValue: string) else {
return nil
}
meals.insert(meal)
}
self.name = name
self.coordinates = (latitude, longitude)
self.meals = meals
}
}

如果你的APP与多个web服务进行交互,并且返回的数据不单一,那么就考虑实现多个构造函数来匹配所有可能的呈现方式。
在上面的例子中,通过可选绑定和as?类型指定来将JSON数据映射到模型的属性上。

写一个带错误管理的JSON构造函数

前一个例子实现了一个可选构造器,当反序列化失败时返回nil。另外,你也可以定义一个遵守Error协议的类型和实现一个可在反序列化失败时返回错误的构造器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
enum SerializationError: Error {
case missing(String)
case invaild(String, Any)
}
// 抛出错误的构造器
init(data: [String: Any]) throws {
guard let name = data["name"] as? String else {
throw SerializationError.missing("name")
}
guard let coordinatesJSON = data["coordinates"] as? [String: Double],
let latitude = coordinatesJSON["lat"],
let longitude = coordinatesJSON["lng"]
else {
throw SerializationError.missing("coordinates")
}
let coordinates = (latitude, longitude)
guard case (-90...90, -180...180) = coordinates else {
throw SerializationError.invaild("coordinates", coordinates)
}
guard let mealsJSON = data["meals"] as? [String] else {
throw SerializationError.missing("meals")
}
var meals: Set<Meal> = []
for string in mealsJSON {
guard let meal = Meal(rawValue: string) else {
throw SerializationError.invaild("meals", string)
}
meals.insert(meal)
}
self.name = name
self.coordinates = coordinates
self.meals = meals
}

这里,Restaurtant类里面声明了一个嵌套的SerializationError类型,定义了关联属性缺失和无效属性两种错误情况。这种抛出错误的构造器强于在序列化错误返回nil的构造器,可以抛出明确的交互错误。这次构造函数同样可以验证数据的合法性。

写获取数据源的类方法

一个web应用终端通常在单个JSON响应中返回多个数据源。比如/search终端可能返回0或多个匹配查询参数的餐馆,下面一段数据展示这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"query": "sandwich",
"results_count": 12,
"page": 1,
"results": [
{
"name": "Caffè Macs",
"coordinates": {
"lat": 37.330576,
"lng": -122.029739
},
"meals": ["breakfast", "lunch", "dinner"]
},
...
]
}

你可以在Restaurant结构体中创建一个类方法,将查询条件正确地转化为
请求参数,并向服务器发送一个请求。这段代码同时也响应了服务器请求,解析JSON数据,把从results数组中获取的每一个字典转化为Restaurant对象,并在完成处理时自动返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
extension Restaurant {
private let urlComponents: URLComponents // base URL components of the web service
private let session: URLSession // shared session for interacting with the web service
static func restaurants(matching query: String, completion: ([Restaurant]) -> Void) {
var searchURLComponents = urlComponents
searchURLComponents.path = "/search"
searchURLComponents.queryItems = [URLQueryItem(name: "q", value: query)]
let searchURL = searchURLComponents.url!
session.dataTask(url: searchURL, completion: { (_, _, data, _)
var restaurants: [Restaurant] = []
if let data = data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
for case let result in json["results"] {
if let restaurant = Restaurant(json: result) {
restaurants.append(restaurant)
}
}
}
completion(restaurants)
}).resume()
}
}

在viewController中,当搜索条件发生变化时调用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
extension ViewController: UISearchResultsUpdating {
func updateSearchResultsForSearchController(_ searchController: UISearchController) {
if let query = searchController.searchBar.text, !query.isEmpty {
Restaurant.restaurants(matching: query) { restaurants in
self.restaurants = restaurants
self.tableView.reloadData()
}
}
}
}

用这种方式分离管理为获取餐馆数据提供了便利,即使具体实现发生改变。

映射中的映射

在不同的系统中对相同数据的呈现方式转换对于写软件来说是单调、必要的任务。
因为这些呈现方式的结构十分相似,这可能会创建高层级抽象过程去自动在不同呈现间映射。举例来说,一个类可能会为了自动初始化一个模型,利用Swift映射API去定义一种JSONkey值和类里面的属性名称之间的映射关系。例如Mirror
然而,我们会发现这些抽象过程性质在Swift语言特性的常规使用场景上并没有提供客观的收益,反而在调试问题和处理额外情景上带来了更多的困扰。通过上面的例子,构造器不但从JSON数据中获取和映射数据,还构造了混合数据类型和执行范围有效性验证。
为了实现这些所有的功能,基于映射途径可能会有较大长度。 对于我们的APP来说,评估效率时应该考虑这些。一些重复代码的消耗比使用错误的映射过程更有意义。