[HandyJSON] 在Swift語言中處理JSON - 轉換JSON和Model

weixin_33936401發表於2016-10-02

背景

JSON是移動端開發常用的應用層資料交換協議。最常見的場景便是,客戶端向服務端發起網路請求,服務端返回JSON文字,然後客戶端解析這個JSON文字,再把對應資料展現到頁面上。

但在程式設計的時候,處理JSON是一件麻煩事。在不引入任何輪子的情況下,我們通常需要先把JSON轉為Dictionary,然後還要記住每個資料對應的Key,用這個Key在Dictionary中取出對應的Value來使用。這個過程我們會犯各種錯誤:

  • Key拼寫錯了;
  • 路徑寫錯了;
  • 型別搞錯了;
  • 沒拿到值懵逼了;
  • 某一天和服務端約定的某個欄位變更了,沒能更新所有用到它的地方;
  • ...

為了解決這些問題,很多處理JSON的開源庫應運而生。在Swift中,這些開源庫主要朝著兩個方向努力:

  1. 保持JSON語義,直接解析JSON,但通過封裝使呼叫方式更優雅、更安全;
  2. 預定義Model類,將JSON反序列化為類例項,再使用這些例項;

對於1,使用最廣、評價最好的庫非 SwiftyJSON 莫屬,它很能代表這個方向的核心。它本質上仍然是根據JSON結構去取值,使用起來順手、清晰。但也正因如此,這種做法沒能妥善解決上述的幾個問題,因為Key、路徑、型別仍然需要開發者去指定;

對於2,我個人覺得這是更合理的方式。由於Model類的存在,JSON的解析和使用都受到了定義的約束,只要客戶端和服務端約定好了這個Model類,客戶端定義後,在業務中使用資料時就可以享受到語法檢查、屬性預覽、屬性補全等好處,而且一旦資料定義變更,編譯器會強制所有用到的地方都改過來才能編譯通過,非常安全。這個方向上,開源庫們做的工作,主要就是把JSON文字反序列化到Model類上了。這一類JSON庫有 ObjectMapperJSONNeverDie、以及我開發的 HandyJSON 哈哈。

為什麼用HandyJSON

在Swift中把JSON反序列化到Model類,在HandyJSON出現以前,主要使用兩種方式:

  1. 讓Model類繼承自NSObject,然後class_copyPropertyList()方法獲取屬性名作為Key,從JSON中取得Value,再通過Objective-C runtime支援的KVC機制為類屬性賦值;如JSONNeverDie

  2. 支援純Swift類,但要求開發者實現Mapping函式,使用過載的運算子進行賦值,如ObjectMapper

這兩者都有顯而易見的缺點。前者要求Model繼承自NSObject,非常不優雅,且直接否定了用struct來定義Model的方式;後者的Mapping函式要求開發者自定義,在其中指明每個屬性對應的JSON欄位名,程式碼侵入大,且仍然容易發生拼寫錯誤、維護困難等問題。

HandyJSON另闢蹊徑,採用Swift反射+記憶體賦值的方式來構造Model例項,規避了上述兩個方案遇到的問題,保持原汁原味的Swift類定義。貼一段很能展示這個特點的程式碼:

// 假設這是服務端返回的統一定義的response格式
class BaseResponse<T: HandyJSON>: HandyJSON {
    var code: Int? // 服務端返回碼
    var data: T? // 具體的data的格式和業務相關,故用泛型定義

    public required init() {}
}

// 假設這是某一個業務具體的資料格式定義
struct SampleData: HandyJSON {
    var id: Int?
}

let sample = SampleData(id: 2)
let resp = BaseResponse<SampleData>()
resp.code = 200
resp.data = sample

let jsonString = resp.toJSONString()! // 從物件例項轉換到JSON字串
print(jsonString) // print: {"code":200,"data":{"id":2}}

if let mappedObject = JSONDeserializer<BaseResponse<SampleData>>.deserializeFrom(json: jsonString) { // 從字串轉換為物件例項
    print(mappedObject.data?.id)
}

如果是繼承NSObject類的話,Model定義是沒法用泛型的。

把JSON轉換為Model

簡單型別

某個Model類想支援通過HandyJSON來反序列化,只需要在定義時,實現HandyJSON協議,這個協議只要求實現一個空的init()函式。

class BasicTypes: HandyJSON {
    var int: Int = 2
    var doubleOptional: Double?
    var stringImplicitlyUnwrapped: String!

    required init() {}
}

然後假設我們從服務端拿到這樣一個JSON文字:

let jsonString = "{\"doubleOptional\":1.1,\"stringImplicitlyUnwrapped\":\"hello\",\"int\":1}"

引入HandyJSON以後,我們就可以這樣來做反序列化了:

if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
    // …
}

簡單吧~

支援Struct

如果Model的定義是struct,由於Swift中struct提供了預設建構函式,所以就不需要再實現空的init()函式了。但需要注意,如果你為strcut指定了別的建構函式,那麼就需要保留一個空的實現。

struct BasicTypes: HandyJSON {
    var int: Int = 2
    var doubleOptional: Double?
    var stringImplicitlyUnwrapped: String!
}

let jsonString = "{\"doubleOptional\":1.1,\"stringImplicitlyUnwrapped\":\"hello\",\"int\":1}"
if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
    // …
}

支援列舉

支援值型別的enum,只需要宣告服從HandyJSONEnum協議。不再需要其他特殊處理了。

enum AnimalType: String, HandyJSONEnum {
    case Cat = "cat"
    case Dog = "dog"
    case Bird = "bird"
}

struct Animal: HandyJSON {
    var name: String?
    var type: AnimalType?
}

let jsonString = "{\"type\":\"cat\",\"name\":\"Tom\"}"
if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
    print(animal.type?.rawValue)
}

較複雜的型別,如可選、隱式解包可選、集合等

HandyJSON支援這些非基礎型別,包括巢狀結構。

class BasicTypes: HandyJSON {
    var bool: Bool = true
    var intOptional: Int?
    var doubleImplicitlyUnwrapped: Double!
    var anyObjectOptional: Any?

    var arrayInt: Array<Int> = []
    var arrayStringOptional: Array<String>?
    var setInt: Set<Int>?
    var dictAnyObject: Dictionary<String, Any> = [:]

    var nsNumber = 2
    var nsString: NSString?

    required init() {}
}

let object = BasicTypes()
object.intOptional = 1
object.doubleImplicitlyUnwrapped = 1.1
object.anyObjectOptional = "StringValue"
object.arrayInt = [1, 2]
object.arrayStringOptional = ["a", "b"]
object.setInt = [1, 2]
object.dictAnyObject = ["key1": 1, "key2": "stringValue"]
object.nsNumber = 2
object.nsString = "nsStringValue"

let jsonString = object.toJSONString()!

if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
    // ...
}

指定解析路徑

HandyJSON支援指定從哪個具體路徑開始解析,反序列化到Model。

class Cat: HandyJSON {
    var id: Int64!
    var name: String!

    required init() {}
}

let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}"

if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
    print(cat.name)
}

有繼承關係的Model類

如果某個Model類繼承自另一個Model類,只需要這個父Model類實現HandyJSON協議就可以:

class Animal: HandyJSON {
    var id: Int?
    var color: String?

    required init() {}
}


class Cat: Animal {
    var name: String?

    required init() {}
}

let jsonString = "{\"id\":12345,\"color\":\"black\",\"name\":\"cat\"}"

if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
    print(cat)
}

自定義解析方式

HandyJSON支援自定義對映關係,或者自定義解析過程。你需要實現一個可選的mapping函式,在裡邊實現NSString值(HandyJSON會把對應的JSON欄位轉換為NSString)轉換為你需要的欄位型別。

所以無法直接支援的型別,都可以用這個方式支援。

class Cat: HandyJSON {
    var id: Int64!
    var name: String!
    var parent: (String, String)?

    required init() {}

    func mapping(mapper: HelpingMapper) {
        // specify 'cat_id' field in json map to 'id' property in object
        mapper <<<
            self.id <-- "cat_id"

        // specify 'parent' field in json parse as following to 'parent' property in object
        mapper <<<
            self.parent <-- TransformOf<(String, String), String>(fromJSON: { (rawString) -> (String, String)? in
                if let parentNames = rawString?.characters.split(separator: "/").map(String.init) {
                    return (parentNames[0], parentNames[1])
                }
                return nil
            }, toJSON: { (tuple) -> String? in
                if let _tuple = tuple {
                    return "\(_tuple.0)/\(_tuple.1)"
                }
                return nil
            })
    }
}

let jsonString = "{\"cat_id\":12345,\"name\":\"Kitty\",\"parent\":\"Tom/Lily\"}"

if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
    print(cat.id)
    print(cat.parent)
}

排除指定屬性

如果在Model中存在因為某些原因不能實現HandyJSON協議的非基本欄位,或者不能實現HandyJSONEnum協議的列舉欄位,又或者說不希望反序列化影響某個欄位,可以在mapping函式中將它排除。如果不這麼做,可能會出現未定義的行為。

class NotHandyJSONType {
    var dummy: String?
}

class Cat: HandyJSON {
    var id: Int64!
    var name: String!
    var notHandyJSONTypeProperty: NotHandyJSONType?
    var basicTypeButNotWantedProperty: String?

    required init() {}

    func mapping(mapper: HelpingMapper) {
        mapper >>> self.notHandyJSONTypeProperty
        mapper >>> self.basicTypeButNotWantedProperty
    }
}

let jsonString = "{\"name\":\"cat\",\"id\":\"12345\"}"

if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
    print(cat)
}

把Model轉換為JSON文字

HandyJSON還支援物件到字典、到JSON字串的序列化功能。

基本型別

現在,序列化也要求Model宣告服從HandyJSON協議。

class BasicTypes: HandyJSON {
    var int: Int = 2
    var doubleOptional: Double?
    var stringImplicitlyUnwrapped: String!

    required init() {}
}

let object = BasicTypes()
object.int = 1
object.doubleOptional = 1.1
object.stringImplicitlyUnwrapped = “hello"

print(object.toJSON()!) // 序列化到字典
print(object.toJSONString()!) // 序列化到JSON字串
print(object.toJSONString(prettyPrint: true)!) // 序列化為格式化後的JSON字串

自定義對映和排除

和反序列化一樣,只要定義mappingexclude就可以了。被排除的屬性,序列化和反序列化都不再影響到它。而在mapping中定義的Transformer,同時定義了序列化和反序列的規則,所以只要為屬性指明一個Transformer關係就可以了。

總結

有了HandyJSON的支援,現在我們可以開心地在Swift中使用JSON了。這個庫還在更新中,已經支援了Swift 2.2+, Swift 3.0+。如果大家有什麼需求或者建議,快去 https://github.com/alibaba/handyjson 給作者(哈哈沒錯就是我)提issue吧~~

相關文章