toppic
当前位置: 首页> 穿越小说> Swift 中的 JSON 解析

Swift 中的 JSON 解析

2020-08-31 16:24:23

(点击上方公众号,可快速关注)

来源:伯乐在线 - 古鲁伊

原文:developer.apple.com

译文:http://ios.jobbole.com/90465/


相比于主流的 Swift JSON 库使用自动映射来处理 JSON 转模型,苹果官方反而鼓励使用简单直接的方式。这也是我一直在用的。 阅读原文 »


Swift 中的 JSON 解析


如果 app 与 web 应用通信,那么从服务端返回的消息通常是 JSON 格式的。Foundation 框架的 JSONSerialization 类可以将 JSON 转换为 Swift 的 Dictionary、Array、String、Number和 Bool 等数据类型。但是因为无法确定 app 接收的 JSON 结构或值,所以很难正确地反序列化对象模型。本文列举了一些 app 中使用 JSON 的方法。


从 JSON 取值


JSONSerialization 类方法 jsonObject(with:options:) 可以返回 Any 类型的值,如果数据无法解析则会抛出 error。


import Foundation

 

let data: Data // received from a network request, for example

let json = try? JSONSerialization.jsonObject(with: data, options: [])


即使有效的 JSON 可能只包含一个简单的值,web 应用的响应结果也通常会将 object 或 array 编码,作为顶层对象。在 if 或 guard 语句中使用可选绑定和 as? 类型强制转换运算符可以将未知类型的值提取为常量。要从一个 JSON 对象类型获得 Dictionary 值,可以按条件强制转换为 [String: Any]。要从 JSON 数组类型获得 Array 值,可以按条件将其强制转换为 [Any](或元素类型更具体的数组,比如 [String])。使用非强制绑定下标访问器或枚举模式匹配的类型强制转换,可以用 key 获取 dictionary 值或用 index 获取 array 值。


// Example JSON with object root:

/*

{

"someKey": 42.0,

"anotherKey": {

"someNestedKey": true

}

}

*/

if let dictionary = jsonWithObjectRoot as? [String: Any] {

if let number = dictionary["someKey"] as? Double {

// access individual value in dictionary

}

 

for (key, value) in dictionary {

// access all key / value pairs in dictionary

}

 

if let nestedDictionary = dictionary["anotherKey"] as? [String: Any] {

// access nested dictionary values by key

}

}

 

// Example JSON with array root:

/*

[

"hello", 3, true

]

*/

if let array = jsonWithArrayRoot as? [Any] {

if let firstObject = array.first {

// access individual object in array

}

 

for object in array {

// access all objects in array

}

 

for case let string as String in array {

// access only string values in array

}

}


Swift’s built-in language features make it easy to safely extract and work with JSON data decoded with Foundation APIs — without the need for an external library or framework.


Swift 固有语言的特点使得安全获取和使用 Foundation API(无需额外的库或框架)解码的 JSON 数据很容易


根据 JSON 获取到的值创建模型对象


大部分 Swift app 都遵循 Model-View-Controller 设计模式,所以经常将 JSON 数据转化为模型定义中针对 app 域名的具体对象。


例如,当编写一个 app,提供当地餐馆的搜索结果时,可能会用接收 JSON 对象的初始化式,和向服务器的 /search 终端发起 HTTP 请求,之后异步返回一个 Restauran 对象数组的类型方法来实现一个 Restaurant 模型。


看看下面的 Restaurant 模型:


import Foundation

 

struct Restaurant {

enum Meal: String {

case breakfast, lunch, dinner

}

 

let name: String

let location: (latitude: Double, longitude: Double)

let meals: Set<Meal>

}


一个 Restaurant 拥有一个 String 类型的 name,一个二维坐标表示的 location 和包含嵌套 Meal 枚举值的 meals Set。


下例展示了在服务端响应中,一个餐馆是如何表示的。


{

"name": "Caffè Macs",

"coordinates": {

"lat": 37.330576,

"lng": -122.029739

},

"meals": ["breakfast", "lunch", "dinner"]

}


编写可选 JSON 的初始化式


要将 JSON 表述转换为 Restaurant 对象,可以编写一个带有获取 JSON 数据并转化为属性的 Any 参数的初始化式。


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 字典的每个值都被提取为常量了。对 name 属性来说,按原样赋值给提取的 name 值。对 coordinate 属性来说,赋值前将提取的 latitude、longitude 值结合为元组。对 meals 属性来说,迭代提取的字符串值以构造 Meal 枚举值的 Set。


编写代有错误处理的 JSON 初始化式


之前的例子实现了一个若反序列化失败则会返回 nil 的可选初始化式。也可以定义遵守 Error 协议的类型,实现一个每当反序列化失败时都会抛出相应类型 error 的初始化式。


enum SerializationError: Error {

case missing(String)

case invalid(String, Any)

}

 

extension Restaurant {

init(json: [String: Any]) throws {

// Extract name

guard let name = json["name"] as? String else {

throw SerializationError.missing("name")

}

 

// Extract and validate coordinates

guard let coordinatesJSON = json["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.invalid("coordinates", coordinates)

}

 

// Extract and validate meals

guard let mealsJSON = json["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.invalid("meals", string)

}

 

meals.insert(meal)

}

 

// Initialize properties

self.name = name

self.coordinates = coordinates

self.meals = meals

}

}


这里的 Restaurant 类型声明了一个嵌套的 SerializationError 类型,为丢失或不合法的属性定义了带有关联值的枚举情况。此版本的 JSON 初始化式,并没有通过返回 nil 来标志失败,而是通过抛出 error 以说明具体的失败原因。这一版本也对 input 数据进行了验证,以保证 coordinates 是有效的地理坐标对,并且 JSON 中具体 meals 的每个名字都对应 Meal 枚举情况。


编写类型方法提取结果


Web 应用终端通常会在一个 JSON 中返回复杂的资源。例如,一个 /search 终端可能返回零或多个匹配请求查询参数的餐馆,包括其它元数据的表述:


{

"query": "sandwich",

"results_count": 12,

"page": 1,

"results": [

{

"name": "Caffè Macs",

"coordinates": {

"lat": 37.330576,

"lng": -122.029739

},

"meals": ["breakfast", "lunch", "dinner"]

},

...

]

}


这时可以在 Restaurant 结构上创建一个类型方法,将 query 方法参数转化为相应的请求对象,并向 web 服务发送 HTTP 请求。这段代码同样可以处理响应、反序列化 JSON 数据、根据 "results" 数组抽取的每项字典创建 Restaurant 对象以及在完成处理器中将它们异步返回。


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()

}

}


当用户在搜索条中输入文本时,view controller 可以调用这个方法,用匹配的餐馆数据填充表格视图:


import UIKit

 

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()

}

}

}

}


这样的关注点分离可以为 view controller 访问餐馆资源提供一致的接口,即使 web 服务的实现细节变化了也没关系。


关于反射的反思


为了不同系统间的通信,相同数据表述之间的转换对编写软件来说是一项乏味但必要的任务。


因为这些表述的结构可能非常相似,所以建立高层次的抽象以自动比对这些不同表述可能很有吸引力。例如,一种类型可能定义了 snake_case JSON 键和 camelCase 属性名间的映射,这样就可以根据 JSON,使用 Swift reflection API(比如 Mirror)自动初始化模型。


但是我们发现这种抽象在Swift语言特点下,日常使用起来意义不大,反而使得 debug 问题或处理边界情况更加困难了。在上述例子中,初始化式不仅从 JSON 提取、映射出值,同时可以初始化复杂的数据类型、执行特定域名的输入验证。为了实现这些任务,基于映射的方式成本更高。评估 app 可用策略时记住:少量重复的成本可能会比采用不当抽象的成本小得多。


专栏作者简介 )


古鲁伊 :立志做一名有格调的程序媛

打赏支持作者写出更多好文章,谢谢!



关注「 iOS大全 」

看更多精选 iOS 技术文章

↓↓↓


友情链接