在Swift语言中处理JSON - 转换JSON和Model

tsmd6369 5年前
   <h2><strong>背景</strong></h2>    <p>JSON是移动端开发常用的应用层数据交换协议。最常见的场景便是,客户端向服务端发起网络请求,服务端返回JSON文本,然后客户端解析这个JSON文本,再把对应数据展现到页面上。</p>    <p>但在编程的时候,处理JSON是一件麻烦事。在不引入任何轮子的情况下,我们通常需要先把JSON转为Dictionary,然后还要记住每个数据对应的Key,用这个Key在Dictionary中取出对应的Value来使用。这个过程我们会犯各种错误:</p>    <ul>     <li> <p>Key拼写错了;</p> </li>     <li> <p>路径写错了;</p> </li>     <li> <p>类型搞错了;</p> </li>     <li> <p>没拿到值懵逼了;</p> </li>     <li> <p>某一天和服务端约定的某个字段变更了,没能更新所有用到它的地方;</p> </li>     <li> <p>...</p> </li>    </ul>    <p>为了解决这些问题,很多处理JSON的开源库应运而生。在Swift中,这些开源库主要朝着两个方向努力:</p>    <ol>     <li> <p>保持JSON语义,直接解析JSON,但通过封装使调用方式更优雅、更安全;</p> </li>     <li> <p>预定义Model类,将JSON反序列化为类实例,再使用这些实例;</p> </li>    </ol>    <p>对于1,使用最广、评价最好的库非 SwiftyJSON 莫属,它很能代表这个方向的核心。它本质上仍然是根据JSON结构去取值,使用起来顺手、清晰。但也正因如此,这种做法没能妥善解决上述的几个问题,因为Key、路径、类型仍然需要开发者去指定;</p>    <p>对于2,我个人觉得这是更合理的方式。由于Model类的存在,JSON的解析和使用都受到了定义的约束,只要客户端和服务端约定好了这个Model类,客户端定义后,在业务中使用数据时就可以享受到语法检查、属性预览、属性补全等好处,而且一旦数据定义变更,编译器会强制所有用到的地方都改过来才能编译通过,非常安全。这个方向上,开源库们做的工作,主要就是把JSON文本反序列化到Model类上了。这一类JSON库有 ObjectMapper 、 JSONNeverDie 、 HandyJSON 等。而 HandyJSON 是其中使用最舒服的一个库,本文将介绍用   HandyJSON 来进行Model和JSON间的互相转换。</p>    <h2><strong>为什么用HandyJSON</strong></h2>    <p>在Swift中把JSON反序列化到Model类,在 HandyJSON 出现以前,主要使用两种方式:</p>    <ol>     <li> <p>让Model类继承自 NSObject ,然后 class_copyPropertyList() 方法获取属性名作为Key,从JSON中取得Value,再通过 Objective-C runtime 支持的 KVC 机制为类属性赋值;如 JSONNeverDie ;</p> </li>     <li> <p>支持纯Swift类,但要求开发者实现 Mapping 函数,使用重载的运算符进行赋值,如 ObjectMapper ;</p> </li>    </ol>    <p>这两者都有显而易见的缺点。前者要求Model继承自 NSObject ,非常不优雅,且直接否定了用 struct 来定义Model的方式;后者的 Mapping 函数要求开发者自定义,在其中指明每个属性对应的JSON字段名,代码侵入大,且仍然容易发生拼写错误、维护困难等问题。</p>    <p>而 HandyJSON 独辟蹊径,采用 Swift反射 + 内存赋值 的方式来构造Model实例,规避了上述两个方案遇到的问题。</p>    <h2><strong>把JSON转换为Model</strong></h2>    <h3><strong>简单类型</strong></h3>    <p>某个Model类想支持通过 HandyJSON 来反序列化,只需要在定义时,实现 HandyJSON 协议,这个协议只要求实现一个空的 init() 函数。</p>    <p>比如我们和服务端约定了一个 Animal 数据,里面有 name / id / num 字段,那么我们这样定义 Animal 类:</p>    <pre>  <code class="language-swift">class Animal: HandyJSON {      var name: String?      var id: String?      var num: Int?        required init() {} // 如果定义是struct,连init()函数都不用声明;  }</code></pre>    <p>然后假设我们从服务端拿到这样一个JSON文本:</p>    <pre>  <code class="language-swift">let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"</code></pre>    <p>引入 HandyJSON 以后,我们就可以这样来做反序列化了:</p>    <pre>  <code class="language-swift">if let animal = JSONDeserializer              .deserializeFrom(json: jsonString) {      print(animal.name)      print(animal.id)      print(animal.num)  }      </code></pre>    <p>简单吧~</p>    <h3><strong>比较复杂的类型</strong></h3>    <p>HandyJSON 支持在类定义里使用各种形式的基本属性,包括可选(?),隐式解包可选(!),数组(Array),字典(Dictionary),Objective-C基本类型(NSString、NSNumber),各种类型的嵌套([Int]?、[String]?、[Int]!、...)等等。比如下面这个看起来比较复杂的类型:</p>    <pre>  <code class="language-swift">struct Cat: HandyJSON {      var id: Int64!      var name: String!      var friend: [String]?      var weight: Double?      var alive: Bool = true      var color: NSString?  }</code></pre>    <p>一样轻松转换:</p>    <pre>  <code class="language-swift">let jsonString = "{\"id\":1234567,\"name\":\"Kitty\",\"friend\":[\"Tom\",\"Jack\",\"Lily\",\"Black\"],\"weight\":15.34,\"alive\":false,\"color\":\"white\"}"    if let cat = JSONDeserializer              .deserializeFrom(json: jsonString) {      print(cat.xxx)  }      </code></pre>    <h3><strong>嵌套的Model类</strong></h3>    <p>如果Model类中的某个属性是另一个自定义的Model类,那么只要那个Model类也实现了 HandyJSON 协议,就一样可以转换:</p>    <pre>  <code class="language-swift">struct Component: HandyJSON {      var aInt: Int?      var aString: String?  }    struct Composition: HandyJSON {      var aInt: Int?      var comp1: Component?      var comp2: Component?  }    let jsonString = "{\"num\":12345,\"comp1\":{\"aInt\":1,\"aString\":\"aaaaa\"},\"comp2\":{\"aInt\":2,\"aString\":\"bbbbb\"}}"    if let composition = JSONDeserializer              .deserializeFrom(json: jsonString) {      print(composition)  }      </code></pre>    <h3><strong>指定反序列化JSON中某个节点</strong></h3>    <p>有时候服务端返回给我们的JSON文本包含了大量的状态信息,和Model无关,比如 statusCode , debugMessage 等,或者有用的数据是在某个节点以下,那么我们可以指定反序列化哪个节点:</p>    <pre>  <code class="language-swift">struct Cat: HandyJSON {      var id: Int64!      var name: String!  }    // 服务端返回了这个JSON,我们想解析的只有data里的cat  let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}"    // 那么,我们指定解析 "data.cat",通过点来表达路径  if let cat = JSONDeserializer              .deserializeFrom(json: jsonString, designatedPath: "data.cat") {      print(cat.name)  }      </code></pre>    <h3><strong>有继承关系的Model类</strong></h3>    <p>如果某个Model类继承自另一个 Model 类,只需要这个父Model类实现 HandyJSON 协议就可以:</p>    <pre>  <code class="language-swift">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              .deserializeFrom(json: jsonString) {      print(cat)  }      </code></pre>    <h3><strong>自定义解析方式</strong></h3>    <p>HandyJSON 还提供了一个扩展能力,就是允许自行定义Model类某个字段的解析Key、解析方式。我们经常会有这样的需求:</p>    <ul>     <li> <p>某个Model中,我们不想使用和服务端约定的key作为属性名,想自己定一个;</p> </li>     <li> <p>有些类型如 enum 、 tuple 是无法直接从JSON中解析出来的,但我们在Model类中有这样的属性;</p> </li>    </ul>    <p>HandyJSON 协议提供了一个可选的 mapping() 函数,我们可以在其中指定某个字段用什么Key、或者用什么方法从JSON中解析出它的值。如我们有一个Model类和一个服务端返回的JSON串:</p>    <pre>  <code class="language-swift">class Cat: HandyJSON {      var id: Int64!      var name: String!      var parent: (String, String)?        required init() {}  }    let jsonString = "{\"cat_id\":12345,\"name\":\"Kitty\",\"parent\":\"Tom/Lily\"}"</code></pre>    <p>可以看到, Cat 类的 id 属性和JSON文本中的Key是对应不上的;而对于 parent 这个属性来说,它是一个元组,做不到从JSON中的 "Tom/Lily" 解析出来。所以我们要定义一个 Mapping 函数来做这两个支持:</p>    <pre>  <code class="language-swift">class Cat: HandyJSON {      var id: Int64!      var name: String!      var parent: (String, String)?        required init() {}        func mapping(mapper: HelpingMapper) {          // 指定 id 字段用 "cat_id" 去解析          mapper.specify(property: &id, name: "cat_id")            // 指定 parent 字段用这个方法去解析          mapper.specify(property: &parent) { (rawString) -> (String, String) in              let parentNames = rawString.characters.split{$0 == "/"}.map(String.init)              return (parentNames[0], parentNames[1])          }      }  }</code></pre>    <p>就这样, HandyJSON 完美地帮我们进行了JSON到Model类的转换。真是太Handy了。</p>    <h2><strong>把Model转换为JSON文本</strong></h2>    <p>HandyJSON 还提供了把Model类序列化为JSON文本的能力,简直无情。</p>    <h3>基本类型</h3>    <p>如果只需要进行序列化,那么在定义Model类时,不需要做任何特殊的改动。任何一个类的实例,直接调用 HandyJSON 的序列化方法去序列化,就能得到JSON字符串了。</p>    <pre>  <code class="language-swift">class Animal {      var name: String?      var height: Int?        init(name: String, height: Int) {          self.name = name          self.height = height      }  }    let cat = Animal(name: "cat", height: 30)  print(JSONSerializer.serializeToJSON(object: cat)!)  print(JSONSerializer.serializeToJSON(object: cat, prettify: true)!)</code></pre>    <p>可以通过 prettify 参数来指定获得的是否是格式化后的JSON串。</p>    <h3><strong>复杂类型</strong></h3>    <p>即使Model类中有别的Model类啥的,都一样支持。</p>    <pre>  <code class="language-swift">enum Gender: String {      case Male = "male"      case Female = "Female"  }    struct Subject {      var id: Int64?      var name: String?        init(id: Int64, name: String) {          self.id = id          self.name = name      }  }    class Student {      var name: String?      var gender: Gender?      var subjects: [Subject]?  }    let student = Student()  student.name = "Jack"  student.gender = .Female  student.subjects = [Subject(id: 1, name: "math"), Subject(id: 2, name: "English"), Subject(id: 3, name: "Philosophy")]    print(JSONSerializer.serializeToJSON(object: student)!)  print(JSONSerializer.serializeToJSON(object: student, prettify: true)!)</code></pre>    <h2><strong>总结</strong></h2>    <p>有了 HandyJSON 的支持,现在我们可以开心地在Swift中使用JSON了。这个库还在更新中,已经支持了Swift 2.2+, Swift 3.0+。</p>    <p> </p>    <p>来自:http://www.cocoachina.com/swift/20161010/17711.html</p>    <p> </p>