Swift 4.0 | JSON数据的解析和编码

文 / 菲拉兔

自己撸的图
要求:
  • Platform: iOS8.0+
  • Language: Swift4.0
  • Editor: Xcode9
【问题补充2017-09-28】

最近我发现了一个问题:在Swift4.0中对JSON数据进行解析的时候,如果还用老的JSONSerialization类的话,会出现一个BUG:

  • 问题: 比如我有一个NSObject的类叫Student,其中包含一个var name = ""属性,那么在以上方法解析JSON数据的过程中,name的值将不被写入,这应该是Swift4.0的一个BUG;
  • 解决方法
    1. 用其他的名字替代name字段(暂时发现只有对这个属性不起作用),例如var sname = ""
    1. JSONDecoder新的方式去解析;

Swift4.0以前和OC时代的JSON数据处理

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

Swift4.0以前的JSON解析/编码,和OC时代一样,都是通过NSJSONSerialization类的一些类方法进行处理的

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

// 数据获取
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`")
}

// 数据整理
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,因为不占存储空间,但在实际开发中,如果用Struct去存储解析出来的JSON数据,还是比较麻烦的,尤其是在JSON序列化方面。下面用Class代替Struct演示Class在JSON序列化过程中的方便之处。

// 数据解析和处理全封装在数据Model里,更体现封装性
class GroceryProduct: NSObject{
    var name = ""
    var points = 0
    var descript = ""
    
    override func setValue(_ value: Any?, forKey key: String) {
        // 拦截并进行数据处理
        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 ?? ""
        }
    }
}

// 数据整理
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

    // 将对象中的属性-值转换为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`")
}
// 对象编码为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应满足:

  1. 顶层对象为数组或字典对象
  2. 数组/字典中的所有对象必须为字符串, 数字类型NSNumber, 数组, 字典, 或 NSNull
  3. 所有的字典keys为字符串
  4. 所有的数字对象不能为NaN 或 infinity
    所以struct / class 对象在JSON编码过程中需要自己手动转换成字典/数组,才可以正确被编码为JSON Data,并转换为字符串,然后发给服务器。

Swift4.0中JSON的操作

Swift4.0中利用全新采用JSONDecoder/JSONEncoder类来实现JSON数据的解析和编码。

JSONDecoder

  • 要将JSON Data解析成相应的数据模型,并匹配相应的属性-值,对应的Struct或Class类型要遵守Decodable协议
//Decodable只能解析,不能被编码
struct GroceryProduct: Decodable{
    var name: String
    var points: Int
    var description: String
}

func swift4JSONParser() {
    // 数据获取
    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数组类型
    let decoder = JSONDecoder()
    guard let products = try? decoder.decode([GroceryProduct].self, from: data) else{
        fatalError("`JSON Decode Failed`")
    }
    print(products)
}

Custom Key Names

  • 有些时候,服务器返回的JSON数据中的字段名采用“蛇形”命名法,如果要转成iOS中“驼峰”命名法,就要手动对keys做一次匹配。
  • Swift4.0中,只要指定Struct/Class中的CodingKeys并遵守CodingKeys协议枚举类型属性,并实现对应关系,就可以自动进行匹配替换解析。但注意如果CodingKeys中case没有匹配到JSON中的字段,解析就会失败。
  • 从这一点来说,还是挺麻烦的,不如用Class中的setValue(_ value: Any?, forUndefinedKey key: String),然后匹配指定对应的属性名称。
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 //保持一致,但必须实现所有属性
    }
}

func swift4JSONParser() {
    // 数据获取
    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中对于JSON数据的嵌套结构解析,也有了新的方式,不过还是较为简单。

  • JSON数据:
[
    {
        "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."
            }
        ]
    }
]
  • 定义结构体
struct Product: Decodable {
    var name: String
    var points: Int
    var description: String
}

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

    // 会自动匹配解析成相应的Product对象,因为Product也实现了Decodable协议
    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

多层嵌套数据解析时,有一些结构是我们不需要存储的,这就我们定义一个中间的service模型来临时搭建这个结构。

  • JSON数据:
[
    {
        "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."
                        }
                    }
                ]
            }
        ]
    }
]
  • 定义存储数据模型
struct Product: Decodable {
    var name: String
    var points: Int
    var description: String
}

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

// 中间`架构`类型
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
        }
    }
}

// 扩展接口,实现数据解析
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() {
    // 数据获取
    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`")
    }
    // 数据剥离存储
    let stores = serviceStores.map{ GroceryStore(from: $0) }
    print(stores)
}

Merge Data from Different Depths

合并不同深度层的数据。此时一般要转换成KeyedDecodingContainer进行解析。

  • JSON数据:
{
    "Banana": {
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    "Orange": {
        "points": 100,
        "description": "A juicy orange."
    }
}
  • 数据模型:
struct GroceryStore {
    struct Product {
        let name: String
        let points: Int
        let description: String
    }

    var products: [Product]

    init(products: [Product] = []) {
        self.products = products
    }
}
  • 合并解析
// 扩展增加ProductKey实现CodingKey,便于深层次解析属性
extension GroceryStore {
    struct ProductKey: CodingKey {
// 实现协议方法和属性
        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")!
    }
}

// 扩展实现Decodable协议,并通过decoder.container找到key对应的容器对象
extension GroceryStore: Decodable{
    init(from decoder: Decoder) throws {
        var products = [Product]()
// 找到包含ProductKey中的属性的所有容器
        let container = try decoder.container(keyedBy: ProductKey.self)

// 然后遍历这个容器中的所有key,解析出容器中key对应的数据值
        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() {
    // 数据获取
    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 store = try? decoder.decode(GroceryStore.self, from: data)else{
        fatalError("`JSON Decode Failed`")
    }
    print(store.products)
}

JSONEncoder

要实现包含Struct/Class对象的JSON对象的编码,在Swift4.0中较为简单,只需要遵守Encodable协议,并指定要编码的keys和实现协议encode方法即可。

  • JSON数据:
[
    {
        "name": "Vegetables Store",
        "products": [
            {
                "name": "Banana",
                "points": 200,
                "description": "A banana grown in Ecuador."
            },
            {
                "name": "Orange",
                "points": 100,
                "description": "A juicy orange."
            }
        ]
    }
]
  • 编码实现
struct Product: Decodable {
    let name: String
    let points: Int
    let description: String
}

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

// 实现编码协议
extension GroceryStore: Encodable{
    private enum CodingKeys: CodingKey{
        case name
        case products
    }
    
// 封装要编码的数据结构
    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为实现了Encodable协议的对象
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)
}
补充2017-09-22

在有些情况下,需要struct对象中的某些属性不是全部需要被存储和解析,就需要手动进行decode了

  • 定义结构体
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{
    // 必须实现所有属性 - 初始值
    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)
        
        // 以下属性为可选解析的,设置默认值
        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 = ""
        }
    }
}

如果对你有帮助,别忘了点个❤️并关注下我哦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容