Swift 4.0 | JSON數(shù)據(jù)的解析和編碼

文 / 菲拉兔

自己擼的圖
要求:
  • Platform: iOS8.0+
  • Language: Swift4.0
  • Editor: Xcode9
【問題補(bǔ)充2017-09-28】

最近我發(fā)現(xiàn)了一個(gè)問題:在Swift4.0中對(duì)JSON數(shù)據(jù)進(jìn)行解析的時(shí)候尝盼,如果還用老的JSONSerialization類的話鲤桥,會(huì)出現(xiàn)一個(gè)BUG:

  • 問題: 比如我有一個(gè)NSObject的類叫Student腺阳,其中包含一個(gè)var name = ""屬性骤铃,那么在以上方法解析JSON數(shù)據(jù)的過程中一忱,name的值將不被寫入许饿,這應(yīng)該是Swift4.0的一個(gè)BUG胖烛;
  • 解決方法
    1. 用其他的名字替代name字段(暫時(shí)發(fā)現(xiàn)只有對(duì)這個(gè)屬性不起作用)产雹,例如var sname = ""谬莹;
    1. JSONDecoder新的方式去解析檩奠;

Swift4.0以前和OC時(shí)代的JSON數(shù)據(jù)處理

Swift(1..<4)& Objective-C

Swift4.0以前的JSON解析/編碼约素,和OC時(shí)代一樣,都是通過NSJSONSerialization類的一些類方法進(jìn)行處理的

  • JSON解析
struct GroceryProduct{
    var name: String
    var points: Int
    var description: String
}

// 數(shù)據(jù)獲取
guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
    let data = try? Data.init(contentsOf: fileURL) else{
        fatalError("`JSON File Fetch Failed`")
}

// JSON序列化
guard let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers), 
    let array = json as? [[String: Any]] else{
        fatalError("`JSON Data Serialize Failed`")
}

// 數(shù)據(jù)整理
var products = [GroceryProduct]()
for dict in array {
    products.append(GroceryProduct.init(name: dict["name"] as? String ?? "", 
                                        points: dict["points"] as? Int ?? 0, 
                                        description: dict["description"] as? String ?? ""))
}
print(products)

Note:Swift編程中官方推薦用Struct代替Class笆凌,因?yàn)椴徽即鎯?chǔ)空間圣猎,但在實(shí)際開發(fā)中,如果用Struct去存儲(chǔ)解析出來的JSON數(shù)據(jù)乞而,還是比較麻煩的送悔,尤其是在JSON序列化方面。下面用Class代替Struct演示Class在JSON序列化過程中的方便之處爪模。

// 數(shù)據(jù)解析和處理全封裝在數(shù)據(jù)Model里欠啤,更體現(xiàn)封裝性
class GroceryProduct: NSObject{
    var name = ""
    var points = 0
    var descript = ""
    
    override func setValue(_ value: Any?, forKey key: String) {
        // 攔截并進(jìn)行數(shù)據(jù)處理
        if key == "points" {
            points = (value as? Int) ?? 10
        }
        else{
            super.setValue(value, forKey: key)
        }
    }
    // 未定義key的處理
    override func setValue(_ value: Any?, forUndefinedKey key: String) {
        if key == "description" {
            descript = value as? String ?? ""
        }
    }
}

// 數(shù)據(jù)整理
var products = [GroceryProduct]()
for dict in array {
    let product = GroceryProduct()
    product.setValuesForKeys(dict)
    products.append(product)
}
print(products)
  • JSON編碼
// JSON編碼
struct GroceryProduct{
    var name: String
    var points: Int
    var description: String

    // 將對(duì)象中的屬性-值轉(zhuǎn)換為JSON字典
    func JSONDictionary(ignored keys: [String] = []) -> [String: Any] {
        var dictionary = [String: Any]()
        
        let mirror = Mirror.init(reflecting: self)
        for (key, value) in mirror.children {
            guard let key = key else{
                continue
            }
            guard !keys.contains(key) else{
                continue
            }
            dictionary.updateValue(value, forKey: key)
        }
        
        return dictionary
    }
}

// 需要編碼的JSON Object
var jsonArray = [[String: Any]]()
for product in products {
    jsonArray.append(product.JSONDictionary())
}

// 判斷是否是合法的JSON Object
guard JSONSerialization.isValidJSONObject(jsonArray) else{
    fatalError("`Not Validate JSON Object`")
}
// 對(duì)象編碼為JSON Data,并解析為JSON Text
guard let data = try? JSONSerialization.data(withJSONObject: jsonArray, options: .prettyPrinted), 
    let jsonText = String(data: data, encoding: .utf8) else{
        fatalError("`JSON Object Encode Failed`")
}
print(jsonText)

Note: 合法的JSON Object應(yīng)滿足:

  1. 頂層對(duì)象為數(shù)組或字典對(duì)象
  2. 數(shù)組/字典中的所有對(duì)象必須為字符串, 數(shù)字類型NSNumber, 數(shù)組, 字典, 或 NSNull
  3. 所有的字典keys為字符串
  4. 所有的數(shù)字對(duì)象不能為NaN 或 infinity
    所以struct / class 對(duì)象在JSON編碼過程中需要自己手動(dòng)轉(zhuǎn)換成字典/數(shù)組屋灌,才可以正確被編碼為JSON Data洁段,并轉(zhuǎn)換為字符串,然后發(fā)給服務(wù)器共郭。

Swift4.0中JSON的操作

Swift4.0中利用全新采用JSONDecoder/JSONEncoder類來實(shí)現(xiàn)JSON數(shù)據(jù)的解析和編碼祠丝。

JSONDecoder

  • 要將JSON Data解析成相應(yīng)的數(shù)據(jù)模型,并匹配相應(yīng)的屬性-值除嘹,對(duì)應(yīng)的Struct或Class類型要遵守Decodable協(xié)議
//Decodable只能解析写半,不能被編碼
struct GroceryProduct: Decodable{
    var name: String
    var points: Int
    var description: String
}

func swift4JSONParser() {
    // 數(shù)據(jù)獲取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }
    
// 利用JSONDecoder來解析JSON Data,解析成[GroceryProduct].self數(shù)組類型
    let decoder = JSONDecoder()
    guard let products = try? decoder.decode([GroceryProduct].self, from: data) else{
        fatalError("`JSON Decode Failed`")
    }
    print(products)
}

Custom Key Names

  • 有些時(shí)候尉咕,服務(wù)器返回的JSON數(shù)據(jù)中的字段名采用“蛇形”命名法叠蝇,如果要轉(zhuǎn)成iOS中“駝峰”命名法,就要手動(dòng)對(duì)keys做一次匹配年缎。
  • Swift4.0中悔捶,只要指定Struct/Class中的CodingKeys并遵守CodingKeys協(xié)議枚舉類型屬性,并實(shí)現(xiàn)對(duì)應(yīng)關(guān)系单芜,就可以自動(dòng)進(jìn)行匹配替換解析蜕该。但注意如果CodingKeys中case沒有匹配到JSON中的字段,解析就會(huì)失敗缓溅。
  • 從這一點(diǎn)來說蛇损,還是挺麻煩的赁温,不如用Class中的setValue(_ value: Any?, forUndefinedKey key: String)坛怪,然后匹配指定對(duì)應(yīng)的屬性名稱。
struct GroceryProduct: Decodable{
    var name: String
    var points: Int
    var description: String
    
    // CodingKeys
    private enum CodingKeys: String, CodingKey{
        case name = "product_name"
        case points = "product_points"
        case description //保持一致股囊,但必須實(shí)現(xiàn)所有屬性
    }
}

func swift4JSONParser() {
    // 數(shù)據(jù)獲取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }
    
    let decoder = JSONDecoder()
    guard let products = try? decoder.decode([GroceryProduct].self, from: data) else{
        fatalError("`JSON Decode Failed`")
    }
    print(products)
}

Simple Nested JSON Data

Swift4.0中對(duì)于JSON數(shù)據(jù)的嵌套結(jié)構(gòu)解析袜匿,也有了新的方式,不過還是較為簡單稚疹。

  • JSON數(shù)據(jù):
[
    {
        "name": "Home Town Market",
        "products": [
            {
                "name": "Banana",
                "points": 200,
                "description": "A banana that's perfectly ripe."
            },
            {
                "name": "Apple",
                "points": 200,
                "description": "A banana that's perfectly ripe."
            }
        ]
    }
]
  • 定義結(jié)構(gòu)體
struct Product: Decodable {
    var name: String
    var points: Int
    var description: String
}

struct GroceryStore: Decodable {
    var name: String
    var products: [Product]
}
  • 嵌套解析
func swift4JSONParser() {
    // 數(shù)據(jù)獲取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }

    // 會(huì)自動(dòng)匹配解析成相應(yīng)的Product對(duì)象居灯,因?yàn)镻roduct也實(shí)現(xiàn)了Decodable協(xié)議
    let decoder = JSONDecoder()
    guard let stores = try? decoder.decode([GroceryStore].self, from: data) else{
        fatalError("`JSON Decode Failed`")
    }
    print(stores)

Multiple Level Nested JSON Data

多層嵌套數(shù)據(jù)解析時(shí)祭务,有一些結(jié)構(gòu)是我們不需要存儲(chǔ)的,這就我們定義一個(gè)中間的service模型來臨時(shí)搭建這個(gè)結(jié)構(gòu)怪嫌。

  • JSON數(shù)據(jù):
[
    {
        "name": "Big City Market",
        "aisles": [
            {
                "name": "Sale Aisle",
                "shelves": [
                    {
                        "name": "Seasonal Sale",
                        "product": {
                            "name": "Chestnuts",
                            "points": 700,
                            "description": "Chestnuts that were roasted over an open fire."
                        }
                    },
                    {
                        "name": "Last Season's Clearance",
                        "product": {
                            "name": "Pumpkin Seeds",
                            "points": 400,
                            "description": "Seeds harvested from a pumpkin."
                        }
                    }
                ]
            }
        ]
    }
]
  • 定義存儲(chǔ)數(shù)據(jù)模型
struct Product: Decodable {
    var name: String
    var points: Int
    var description: String
}

struct GroceryStore {
    var name: String
    var products: [Product]
}

// 中間`架構(gòu)`類型
struct GroceryStoreService: Decodable {
    let name: String
    let aisles: [Aisle]
    
    struct Aisle: Decodable {
        let name: String
        let shelves: [Shelf]
        
        struct Shelf: Decodable {
            let name: String
            let product: Product
        }
    }
}

// 擴(kuò)展接口义锥,實(shí)現(xiàn)數(shù)據(jù)解析
extension GroceryStore {
    init(from service: GroceryStoreService) {
        name = service.name
        products = []
        
        for aisle in service.aisles {
            for shelf in aisle.shelves{
                products.append(shelf.product)
            }
        }
    }
}

  • 嵌套解析
func swift4JSONParser() {
    // 數(shù)據(jù)獲取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }
    
    let decoder = JSONDecoder()
    guard let serviceStores = try? decoder.decode([GroceryStoreService].self, from: data)else{
        fatalError("`JSON Decode Failed`")
    }
    // 數(shù)據(jù)剝離存儲(chǔ)
    let stores = serviceStores.map{ GroceryStore(from: $0) }
    print(stores)
}

Merge Data from Different Depths

合并不同深度層的數(shù)據(jù)。此時(shí)一般要轉(zhuǎn)換成KeyedDecodingContainer進(jìn)行解析岩灭。

  • JSON數(shù)據(jù):
{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    "Orange": {
        "points": 100,
        "description": "A juicy orange."
    }
}
  • 數(shù)據(jù)模型:
struct GroceryStore {
    struct Product {
        let name: String
        let points: Int
        let description: String
    }

    var products: [Product]

    init(products: [Product] = []) {
        self.products = products
    }
}
  • 合并解析
// 擴(kuò)展增加ProductKey實(shí)現(xiàn)CodingKey拌倍,便于深層次解析屬性
extension GroceryStore {
    struct ProductKey: CodingKey {
// 實(shí)現(xiàn)協(xié)議方法和屬性
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }

// 自定義keys
        static let points = ProductKey(stringValue: "points")!
        static let description = ProductKey(stringValue: "description")!
    }
}

// 擴(kuò)展實(shí)現(xiàn)Decodable協(xié)議,并通過decoder.container找到key對(duì)應(yīng)的容器對(duì)象
extension GroceryStore: Decodable{
    init(from decoder: Decoder) throws {
        var products = [Product]()
// 找到包含ProductKey中的屬性的所有容器
        let container = try decoder.container(keyedBy: ProductKey.self)

// 然后遍歷這個(gè)容器中的所有key噪径,解析出容器中key對(duì)應(yīng)的數(shù)據(jù)值
        for key in container.allKeys {
            let productContainer = try container.nestedContainer(keyedBy: ProductKey.self, forKey: key)
            let points = try productContainer.decode(Int.self, forKey: .points)
            let description = try productContainer.decode(String.self, forKey: .description)
            
            let product = Product(name: key.stringValue, points: points, description: description)
            products.append(product)
        }
        
        self.init(products: products)
    }
}

func swift4JSONParser() {
    // 數(shù)據(jù)獲取
    guard let fileURL = Bundle.main.url(forResource: "test.json", withExtension: nil), 
        let data = try? Data.init(contentsOf: fileURL) else{
            fatalError("`JSON File Fetch Failed`")
    }
    
    // 數(shù)據(jù)解析
    let decoder = JSONDecoder()
    guard let store = try? decoder.decode(GroceryStore.self, from: data)else{
        fatalError("`JSON Decode Failed`")
    }
    print(store.products)
}

JSONEncoder

要實(shí)現(xiàn)包含Struct/Class對(duì)象的JSON對(duì)象的編碼柱恤,在Swift4.0中較為簡單,只需要遵守Encodable協(xié)議找爱,并指定要編碼的keys和實(shí)現(xiàn)協(xié)議encode方法即可梗顺。

  • JSON數(shù)據(jù):
[
    {
        "name": "Vegetables Store",
        "products": [
            {
                "name": "Banana",
                "points": 200,
                "description": "A banana grown in Ecuador."
            },
            {
                "name": "Orange",
                "points": 100,
                "description": "A juicy orange."
            }
        ]
    }
]
  • 編碼實(shí)現(xiàn)
struct Product: Decodable {
    let name: String
    let points: Int
    let description: String
}

struct GroceryStore: Decodable {
    var name: String
    var products: [Product]
}

// 實(shí)現(xiàn)編碼協(xié)議
extension GroceryStore: Encodable{
    private enum CodingKeys: CodingKey{
        case name
        case products
    }
    
// 封裝要編碼的數(shù)據(jù)結(jié)構(gòu)
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(products, forKey: .products)
    }
}

extension Product: Encodable{
    private enum CodingKeys: CodingKey{
        case name
        case points
        case description
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(points, forKey: .points)
        try container.encode(description, forKey: .description)
    }
}

// 要求object為實(shí)現(xiàn)了Encodable協(xié)議的對(duì)象
func swift4JSONEncode<T: Encodable> (withJSONObject object: T){
    // 編碼
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    guard let encodedData = try? encoder.encode(object), 
        let jsonText = String(data: encodedData, encoding: .utf8) else {
            fatalError("`JSON Encode Failed`")
    }
    print(jsonText)
}
補(bǔ)充2017-09-22

在有些情況下,需要struct對(duì)象中的某些屬性不是全部需要被存儲(chǔ)和解析车摄,就需要手動(dòng)進(jìn)行decode了

  • 定義結(jié)構(gòu)體
struct GitHubUser {
    var id: Int
    var type: String
    var loginName: String
    var avatarUrl: String
    var homepageUrl: String
    var profileUrl: String
    var name: String
    var company: String
    var location: String
    var blog: String
    var bio: String
    
    enum CodingKeys: String, CodingKey{
        case id,type,name,company,location,blog,bio
        case loginName = "login"
        case avatarUrl = "avatar_url"
        case homepageUrl = "html_url"
        case profileUrl = "url"
    }
}
  • 自定義解析
extension GitHubUser: Decodable{
    // 必須實(shí)現(xiàn)所有屬性 - 初始值
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(Int.self, forKey: .id)
        type = try values.decode(String.self, forKey: .type)
        loginName = try values.decode(String.self, forKey: .loginName)
        avatarUrl = try values.decode(String.self, forKey: .avatarUrl)
        homepageUrl = try values.decode(String.self, forKey: .homepageUrl)
        profileUrl = try values.decode(String.self, forKey: .profileUrl)
        
        // 以下屬性為可選解析的寺谤,設(shè)置默認(rèn)值
        do {
            name = try values.decode(String.self, forKey: .name)
        }catch{
            name = ""
        }
        
        do {
            company = try values.decode(String.self, forKey: .company)
        }catch{
            company = ""
        }
        
        do {
            location = try values.decode(String.self, forKey: .location)
        }catch{
            location = ""
        }
        
        do{
            blog = try values.decode(String.self, forKey: .blog)
        }catch{
            blog = ""
        }
        
        do{
            bio = try values.decode(String.self, forKey: .bio)
        }catch{
            bio = ""
        }
    }
}

如果對(duì)你有幫助,別忘了點(diǎn)個(gè)??并關(guān)注下我哦吮播。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末矗漾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子薄料,更是在濱河造成了極大的恐慌敞贡,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摄职,死亡現(xiàn)場離奇詭異誊役,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)谷市,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門蛔垢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人迫悠,你說我怎么就攤上這事鹏漆。” “怎么了创泄?”我有些...
    開封第一講書人閱讀 167,815評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵艺玲,是天一觀的道長。 經(jīng)常有香客問我鞠抑,道長饭聚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評(píng)論 1 296
  • 正文 為了忘掉前任搁拙,我火速辦了婚禮秒梳,結(jié)果婚禮上法绵,老公的妹妹穿的比我還像新娘。我一直安慰自己酪碘,他們只是感情好朋譬,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著兴垦,像睡著了一般此熬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滑进,一...
    開封第一講書人閱讀 52,184評(píng)論 1 308
  • 那天犀忱,我揣著相機(jī)與錄音,去河邊找鬼扶关。 笑死阴汇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的节槐。 我是一名探鬼主播搀庶,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼铜异!你這毒婦竟也來了哥倔?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤揍庄,失蹤者是張志新(化名)和其女友劉穎咆蒿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蚂子,經(jīng)...
    沈念sama閱讀 46,212評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沃测,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了食茎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒂破。...
    茶點(diǎn)故事閱讀 40,438評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖别渔,靈堂內(nèi)的尸體忽然破棺而出附迷,到底是詐尸還是另有隱情,我是刑警寧澤哎媚,帶...
    沈念sama閱讀 36,128評(píng)論 5 349
  • 正文 年R本政府宣布喇伯,位于F島的核電站,受9級(jí)特大地震影響抄伍,放射性物質(zhì)發(fā)生泄漏艘刚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評(píng)論 3 333
  • 文/蒙蒙 一截珍、第九天 我趴在偏房一處隱蔽的房頂上張望攀甚。 院中可真熱鬧,春花似錦岗喉、人聲如沸秋度。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荚斯。三九已至,卻和暖如春查牌,著一層夾襖步出監(jiān)牢的瞬間事期,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評(píng)論 1 272
  • 我被黑心中介騙來泰國打工纸颜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留兽泣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,827評(píng)論 3 376
  • 正文 我出身青樓胁孙,卻偏偏與公主長得像唠倦,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涮较,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容