Swift4 JSON 解析

Swift 的 JSON 解析一直是一件很麻烦的事, 在 Swift3 中请求一个数据后可能要进行如下操作(比如服务器返回一个数组):

if let jsonObject = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) {
  if let objectList = jsonObject as? [Any] {
    for object in objectList {
      if let object = object as? [String: Any] {
        // use object
      }
    }
  }
}

经过千辛万苦终于拿到了这个 object , 然而噩梦才刚刚开始

转成 [String: Any] 的 object 基本上没法用, 很多时候我们还需要去拿每个 key 和 value 再赋给我们自己定义好的 struct / class (又是一长串的 if let as)

当然, 目前解决这个问题还是有一些办法的, 比如可以去尝试使用 SwiftyJSON 直接当原生的 Array / Dictionary 用, 或者更高级的 Argo, 直接将 JSON data 映射到自己定义的 struct

在 Swift4 中, Swift Standard Library 带来的新的类和协议支持原生的 JSON 解析. 甚至不只是 JSON, 只要是 encoding / decoding 的转化都可以支持(比如 plist)

关于 Swift4 Codable

在 Swfit4 中新添加了一个复合协议 typealias Codable = Decodable & Encodable, 想进行 encoding / decoding 只要实现这个协议即可. 而且根据需要, 如果只是从服务器取数据或只是向服务器发数据完全可以只实现其中一个, 接下来先来试验下 Decodable 协议. 先找一个 API, 就随便找了一个项目直接用 github 的 API https://api.github.com/repos/bewils/IWantTheGreenOne. 首先可以看看返回的数据结构: 因为只取了一个 repository 所以可以看到返回的结构也很简单, 按照这个结构写出如下 struct

struct Repo: Decodable {
  var `private`: Bool
  var html_url: String
  var description: String?
}

这里只取了其中的三个属性, 即只解析返回的 data 的这三个字段, 然后尝试发送请求并解析返回的数据

if let url = URL(string: "https://api.github.com/repos/bewils/IWantTheGreenOne") {
  let session = URLSession(configuration: .default)
  session.dataTask(with: url) { (data, _, err) in
    guard err == nil else { return }
    
    guard let data = data else { return }
    if let repo = try? JSONDecoder().decode(Repo.self, from: data) {
      print(repo)
    } else {
      print("JSON parse failed")
    }
  }.resume()
}

奇迹出现了, 很快控制台里就输出了 Repo(private: false, html_url: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game")) 这样的文字. 我们几乎没做什么事, 一如既往的网络请求, 在回调函数中利用同样是 Swift4 中新加入的 JSONDecoder 类来按照 Repo 的模型解析 data, 然后就成功的输出了解析结果

Codable 便利的一点还在于 Swift 中给出了这个协议的默认实现, 就像 Repo 直接遵从 Decodable 没有写解析的任何方法就能通过 JSONDecoder 解析出来

同时在声明 Repo 时我们将 description 声明成了 String? 的类型, 这样如果返回的数据里没有这个字段只会解析出 nil 而不会报错

关于字段名

第一个字段 private 是 Swift 的关键字, 当做变量名的时候只能用 `` 包起来, 那要不换个名字改成 jurisdiction. 运行, 好的, 报错: 没有 jurisdicition 这个字段

这只是一种情况, 还有比如返回的 JSON 的 key 是用 _ 分割的命名, 而 Swift 的代码风格一般是驼峰命名, 这时就会有字段名不对应的问题, 为了解决这个问题, 可以在 Repo 的内部声明一个遵从 CodingKey 的叫 CodingKeys 枚举值

struct Repo: Decodable {
  var jurisdiction: Bool
  var htmlUrl: String
  var description: String?
  
  enum CodingKeys: String, CodingKey {
    case jurisdiction = "private"
    case htmlUrl = "html_url"
    case description
  }
}

重新运行, 又一次成功地解析出了 Repo: Repo(jurisdiction: false, htmlUrl: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game")).

通过使用 CodingKey 的方式重新定义 JSON key 和属性名的对应关系是很好用的, 但这里还有一个小问题, 就是如果使用 CodingKey 就必须吧所有的属性和 key 都写进去, 比如 Repo 中虽然 description 不需要转化还是要写进去, 否则这次会找不到 description 对应的 key (如果有 50 个属性的对象为了一个属性而使用 CodingKeys 也是很惨的...)

关于嵌套结构

可以看到从 API 返回的数据虽然是基本的 Dictionary 结构, 但里面还是有一个 owner 的字段属于嵌套的结构, Swift4 中解析嵌套结构的方法非常简单: 也直接嵌套一个就可以了

struct Owner: Decodable {
  var login: String
  var id: Int
  var avatar_url: String
}

struct Repo: Decodable {
  var jurisdiction: Bool
  var htmlUrl: String
  var description: String?
  var owner: Owner
  
  enum CodingKeys: String, CodingKey {
    case jurisdiction = "private"
    case htmlUrl = "html_url"
    case description
    case owner
  }
}

新添加了一个 Owner 的 struct, 然后加到 Repo 中并且添加一个 CodingKey

运行, 输出: Repo(jurisdiction: false, htmlUrl: "https://github.com/bewils/IWantTheGreenOne", description: Optional("SpriteKit Game"), owner: __lldb_expr_36.Owner(login: "bewils", id: 16081099, avatar_url: "https://avatars3.githubusercontent.com/u/16081099?v=4"))

(简直完美, 此处应有掌声 �👏

关于数组

通过上面的过程已经可以将 Repo 成功解析出来了, 无论多复杂的 JSON, 只要是 key-value, 无论嵌套多深都是一样的写法

然而如果返回的是一个数组呢? https://api.github.com/users/bewils/repos

关于数组的解析将会分为两部分: 二逼程序员和文艺程序员

二逼程序员

再声明一个 stuct

struct UserRepos {
  var repoList: [Repo]
}

因为返回的是一个数组, 没有 key 所以通过默认方法肯定转换不来, 自定义去实现 Decodable

extension UserRepos: Decodable {
  init(from decoder: Decoder) throws {
    repoList = []
    
    var values = try decoder.unkeyedContainer()
    while !values.isAtEnd {
      let repo = try values.decode(Repo.self)
      repoList.append(repo)
    }
  }
}

自定义实现的过程中首先通过 decoder 取出 unkeyedContainer 即初步解析为数组型的结构, 如果这时输出的话可以看到 values 就是我们要的数组, 然后顺手开始 for in, values 的类型是 UnkeyedDecodingContainer, 但竟然不遵从 Sequence 协议…没办法遍历这可是个难题, 而且查看 values 的其他属性 isAtEnd = false, count = 31 也证明了 values 应该是可以遍历的

最后发现 UnkeyedDecodingContainer 的遍历方法就像一个队列, 每次调用 decode 就出一个, 然后通过 isAtEnd 来终止循环就可以了

if let url = URL(string: "https://api.github.com/users/bewils/repos") {
  let session = URLSession(configuration: .default)
  session.dataTask(with: url, completionHandler: { (data, _, err) in
    guard err == nil else { return }
    
    guard let data = data else { return }
    do {
      // 二逼程序员解析法
      let repos = try JSONDecoder().decode(UserRepos.self, from: data)
      print(repos.repoList)
    } catch let err {
      print(err)
    }
  }).resume()
}

这样就通过自定义实现 Decodable 成功地解析出了返回的数组

文艺程序员

文艺程序员的解析方法呢?

let repos = try JSONDecoder().decode([Repo].self, from: data)

最后

Swift4 中提供的 Codable 使得解析 JSON 变的极其方便, 这篇文章中主要讨论了 Decodable, 关于 Encodable 基本上就是 Decodable 的反向操作, 就不在这里讨论了

本文的 demo 代码放在如下地址 SwiftCodable

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

推荐阅读更多精彩内容