開源專案:JSONNeverDie,純 Swift 開發的全功能 JSON 解析、生成庫,相容 SwiftyJSON 主要 API:https://github.com/johnlui/JSONNeverDie
本篇文章中,我將跟大家一起,一步一步構造出一個好用的 JSON 解析和生成的庫。
準備工作
起因
在我動手搞這個 JSON 解析庫之前,我一直在用 SwiftyJSON 這個庫,這個庫是國人開源的最受歡迎的 Swift 專案,沒有之一,也是全球最受歡迎的 Swift 庫第二名,第一名是網路庫 Alamofire。由於要實現 [“key”][“key1”] 這樣的遞迴查詢,我一直覺得 JSON 解析庫非常複雜難搞。
過程
最近比較閒,我打算把之前用過的開源庫都自己實現一下,提升一下自己。而且我在實際使用 SwiftyJSON 的過程中,遇到過非合法長字串導致奔潰的情況,我打算先從 JSON 解析庫下手,於是中秋節的前一天,吃完午飯我就開搞了,到了下午六七點,解析的功能就全部搞定了,十分出乎預料。中秋節這天我又把生成的功能做了,整理下程式碼,收拾收拾就給開源了。
API 統計
言歸正傳,我們的準備工作還是要做的:統計 SwiftyJSON 的主要 API。經過簡單統計,我找到了所有我在專案中使用過的 SwiftyJSON 的 API,主要分為四類:
- 通過特定路徑取出特定型別的值,如:json[“key”][“key1”].stringValue
- 取出某個陣列型別的子 JSON,迴圈拿到裡面的值
- 將某個 JSON 物件格式化成字串
- 使用 Dictionary 生成 JSON 物件
遞迴取值
設計基本結構
既然要相容 SwiftyJSON 的主要 API,那呼叫方式跟它一樣就行了:先使用 NSData、Array 或者 Dictionary 生成 JSON 物件,再對這個物件進行操作,拿到我們想要的值、陣列、完整的 JSON 字串等。
為了對比 API 的執行結果,我們仍然引入 SwiftyJSON 庫,所以我們需要一個其他的類名,在這裡我們就暫定為 JSONND,是 JSON Never Die 的縮寫,含義是永不奔潰的 JSON 解析庫。
我們先從網路資料下手。網路資料的來源一般為 NSData,經過簡單查詢我們知道系統提供了一個 JSON 解析方法,可以把 NSData 格式的解析為 AnyObject,構造出 JSONND 類:
1 2 3 4 5 6 7 8 9 10 11 12 |
public struct JSONND { public var jsonObject: AnyObject! public static func initWithData(data: NSData) -> JSONND! { do { return JSONND(jsonObject: try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)) } catch let error as NSError { let e = NSError(domain: "JSONNeverDie.JSONParseError", code: error.code, userInfo: error.userInfo) NSLog(e.localizedDescription) return JSONND() } } } |
需要注意的是,我們給 JSON 類使用的是 struct 結構體,為了它能夠具備自動初始化函式,值型別等優良特性。JSON 直觀上感覺是 String 的衍生,故使用值型別也起到降低學習成本的作用。
我們使用下面的程式碼來檢驗成果:
1 2 |
let data = NSData(contentsOfURL: NSURL(string: "http://httpbin.org/get?hello=world")!)! let json = JSONND.initWithData(data) |
執行,正常,初始化程式碼完成。
支援 [“key”][“key1”] 形式的遞迴取值
為了支援遞迴取值,同時不讓我們的 JSONND 結構體變的過於臃腫,我們考慮將遞迴取值的任務交給第二個結構體:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public struct JSONNDElement { public var data: AnyObject! public init(data: AnyObject!) { self.data = data } public subscript (index: String) -> JSONNDElement { if let jsonDictionary = self.data as? Dictionary<String, AnyObject> { if let value = jsonDictionary[index] { return JSONNDElement(data: value) } else { NSLog("JSONNeverDie: No such key '\(index)'") } } return JSONNDElement(data: nil) } } |
同時,我們需要在 JSONND 結構體中觸發遞迴取值的第一次:
1 2 3 4 |
public subscript (index: String) -> JSONNDElement { let jsonNDElement = JSONNDElement(data: self.jsonObject) return jsonNDElement[index] } |
檢驗成果:
1 2 3 4 |
let data = NSData(contentsOfURL: NSURL(string: "http://httpbin.org/get?hello=world")!)! let json = JSONND.initWithData(data) let args = json["args"] let hello = args["hello"] |
執行,正常,遞迴取值完成。
取出 Int、Float、String、Array、Bool 型別的值
在我們通過 [“key”][“key1”] 的形式拿到最終的 JSONNDElement 物件之後,我們就需要把他的 data 轉換成我們想要的型別輸出了。介紹 JSON 資料型別的文件:http://www.yiibai.com/json/json_data_types.html
SwiftyJSON 採用兩級函式來取值,即 .int 為 Int? 型別, .intValue 為 Int 型別,這顯然是為了適應不同的 API 設計作出的相容,我們也要實現這樣的兩級取值。要實現取值,其實是非常簡單的,if let 轉換一下型別,基本就 OK 了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public var int: Int? { get { if let _ = self.data { return self.data.integerValue } else { return nil } } } public var intValue: Int { get { if let i = self.int { return i } else { return 0 } } } |
由於程式碼比較繁瑣無趣,這裡只用 Int 展示一下,更多程式碼請見 Github。
Array 型別的處理要單獨拿出來處理,因為 Array 有子級,所以我們得到的將是 JSONNDElement 陣列。Array 處理程式碼如下:
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 |
public var array: [JSONNDElement]? { get { if let _ = self.data { if let arr = self.data as? Array<AnyObject> { var result = Array<JSONNDElement>() for i in arr { result.append(JSONNDElement(data: i)) } return result } else { return nil } } else { return nil } } } public var arrayValue: [JSONNDElement] { get { if let i = self.array { return i } else { return [] } } } |
將 JSONND 物件格式化成字串
通過在 JSONND 和 JSONNDElement 中新增兩個函式,將成員變數 data 轉換成 String 就可以加上這個功能了:
JSONND 中:
1 2 3 4 5 6 7 8 |
public var jsonString: String? { let jsonNDElement = JSONNDElement(data: self.jsonObject) return jsonNDElement.jsonString } public var jsonStringValue: String { let jsonNDElement = JSONNDElement(data: self.jsonObject) return jsonNDElement.jsonStringValue } |
JSONNDElement 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public var jsonString: String? { get { do { if let _ = self.data { return NSString(data: try NSJSONSerialization.dataWithJSONObject(self.data, options: .PrettyPrinted), encoding: NSUTF8StringEncoding) as? String } else { return nil } } catch { return nil } } } public var jsonStringValue: String { get { if let i = self.jsonString { return i } else { return "" } } } |
使用 Array、Dictionary 生成 JSON 物件
這一步操作我們將使用從 SwiftyJSON 中偷來的函式,稍加改裝就可以利用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// stolen from SwiftyJSON extension JSONND: DictionaryLiteralConvertible { public init(dictionaryLiteral elements: (String, AnyObject)...) { self.init(jsonObject: elements.reduce([String : AnyObject]()){(dictionary: [String : AnyObject], element:(String, AnyObject)) -> [String : AnyObject] in var d = dictionary d[element.0] = element.1 return d }) } } // stolen from SwiftyJSON extension JSONND: ArrayLiteralConvertible { public init(arrayLiteral elements: AnyObject...) { self.init(jsonObject: elements) } } |
程式碼的原理也很簡單,利用系統的自動轉換 protocol:DictionaryLiteralConvertible 和 ArrayLiteralConvertible,讓 Array 和 Dictionary 自動轉換為 JSONND 型別。現在我們可以採用這種方式定義 JSONND 物件了:
1 2 |
let dictionaryJSON: JSONND = ["a": 1, "b": [1, 2, 3]] let arrayJSON: JSONND = [0, 1, 2] |
搞定!
檢驗成果
我已經給 JSONNeverDie 專案寫了完整的單元測試來測試每一項功能,感興趣的同學可以去 Github 檢視測試程式碼。