Swift Json解析探索

2018-02-27 11:09:06来源:https://juejin.im/post/5a928d2e5188257a7450d357作者:稀土掘金人点击

分享

客户端开发项目中,不可避免地需要解析网络数据---将服务端下发的JSON数据解析成客户端可阅读友好的Model。Objective-C下使用最多的是 JSONModel ,它能在OC Runtime基础下很好地完成解析工作。那么在纯Swift代码中,这个功能是如何实现的?下面开始我们的探索~


手动解析
原生:Swift4.0 JSONDecoder
JSONDecoder 问题 及 解决方案
手动解析

假设一个User类要解析,Json如下:


{
"userId": 1,
"name": "Jack",
"height": 1.7,
}

对应的创建一个User结构体(也可以是类):


struct User {
var userId: Int?
var name: String?
var height: CGFloat?
}
把JSON转成User

在Swift4.0前,我们以手动解析的方式将JSON model化。给User加一个以JSON为参数的初始化方法,代码如下:


struct User {
...
init?(json: [String: Any]) {
guard let userId = json["userId"] as? Int,
let name = json["name"] as? String,
let height = json["height"] as? CGFloat else { return nil }
self.userId = userId
self.name = name
self.height = height
}
}

依次从json中取出model所需的具体类型的数据,填充到具体对应属性中。如果其中一个转换失败或者没有值,初始化会失败返回nil。


如果某个值不需要强校验,直接取值再赋值,把 guard let 内的语句去掉。例如,若 height 不用校验,可看如下代码:


struct User {
...
init?(json: [String: Any]) {
guard let userId = json["userId"] as? Int,
let name = json["name"] as? String else { return nil }
self.userId = userId
self.name = name
self.height = json["height"] as? CGFloat
}
}
原生:Swift4.0 JSONDecoder

2017年6月份左右Swift4.0发布,其中一个重大更新就是JSON的加解密。摆脱手工解析字段的繁琐,聊聊几行代码就可将JSON转换成Model。与Objective-C下的 JSONModel 极为相似。同样解析上述例子中的User,Swift4.0可以这么写:


struct User: Decodable {
var userId: Int?
var name: String?
var height: CGFloat?
}
let decoder = JSONDecoder()
if let data = jsonString.data(using: String.Encoding.utf8) {
let user = try? decoder.decode(User.self, from: data)
}

so easy~ 与手动解析不同点在于:


移除了手写 init? 方法。不需要手动解了


User 实现 Decodable 协议,协议的定义如下:

/// A type that can decode itself from an external representation.
public protocol Decodable {
/// Creates a new instance by decoding from the given decoder.
///
/// This initializer throws an error if reading from the decoder fails, or
/// if the data read is corrupted or otherwise invalid.
///
/// - Parameter decoder: The decoder to read data from.
public init(from decoder: Decoder) throws
}

Decodable 协议只有一个方法 public init(from decoder: Decoder) throws ---以 Decoder 实例进行初始化,初始化失败可能抛出异常。庆幸的是,只要继承 Decodable 协议,系统会自动检测类中的属性进行初始化工作,省去了人工解析的麻烦~


使用了 JSONDecoder 。它是真正的解析工具,主导整个解析过程


读到这里,是不是觉得人生从黑暗迈向了光明~~


可是,它并不完美...


JSONDecoder问题及方案

解析JSON经常遇到这样两种不一致问题:


服务端下发的key跟端上不一致。比如,服务端下发key="order_id",端上定义key="orderId"
服务端下发的日期表达是 yyyy-MM-dd HH:mm 或者时间戳,但端上是 Date 类型
服务端下发的基本类型和端上定义的不一致。服务端下发的是 String ,端上定义的 Int ,等

前两个问题 JSONDecoder 都能很好地解决。


第一个key不一致问题, JSONDecoder 有现成的方案。以上面介绍的例子来说,假设服务端返回的 key 是 user_id 而不是 userId ,那么我们可以使用 JSONDecoder 的 CodingKeys 像 JSONModel 一样对属性名称在加解密时的名称做转换。 User 修改如下:


struct User: Decodable {
var userId: Int?
var name: String?
var height: CGFloat?
enum CodingKeys: String, CodingKey {
case userId = "user_id"
case name
case height
}
}

第二个, Date 转换问题。 JSONDecoder 也为我们提供了单独的API:


open class JSONDecoder {
/// The strategy to use for decoding `Date` values.
public enum DateDecodingStrategy {
/// Defer to `Date` for decoding. This is the default strategy.
case deferredToDate
/// Decode the `Date` as a UNIX timestamp from a JSON number.
case secondsSince1970
/// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
case millisecondsSince1970
/// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
case iso8601
/// Decode the `Date` as a string parsed by the given formatter.
case formatted(DateFormatter)
/// Decode the `Date` as a custom value decoded by the given closure.
case custom((Decoder) throws -> Date)
}
......
/// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
}

设置好了 JSONDecoder 属性 dateDecodingStrategy 后,解析 Date 类型就会按照指定的策略进行解析。


类型不一致

至此, JSONDecoder 为我们提供了


解析不同 key 值对象
Date 类型可自定义转换
Float 在一些正负无穷及无值得特殊表示。(出现的概率很少,不作具体说明了)

但遇到基本类型端上与服务端不一致时(比如一个数字1,端上的Code是Int型,服务端下发String:"1"), JSONDecoder 会抛出 typeMismatch 异常而终结整个数据的解析。


这让人有点懊恼,端上的应用,我们希望它能够尽可能稳定,而不是某些情况下遇到若干个基本类型不一致整个解析就停止,甚至是 Crash。





如下面表格所示,我们希望类型不匹配时,能够这么处理:左列代表前端的类型,右列代表服务端类型,每一行代表前端类型为X时,能从服务端下发的哪些类型中转化,比如 String 可以从 Int or Float 转化。这几个类型基本能覆盖日常服务端下发的数据,其它类型的转化可根据自己的需求扩充。



前端
服务端


String
Int,Float
Float
String
Double
String
Bool
String, Int

JSONDecoder 没有给我们便利的这种异常处理的API。如何解决呢?最直接的想法,在具体的 model 内实现 init(decoder: Decoder) 手动解析可以实现,但每个都这么处理太麻烦。


解决方案: KeyedDecodingContainer 方法覆盖


研究 JSONDecoder的源码 ,在解析自定义Model过程中,会发现这样一个调用关系。


// 入口方法
JSONDecoder decoder(type:Type data:Data)
// 内部类,真实用来解析的
_JSONDecoder unbox(value:Any type:Type)
// Model调用init方法
Decodable init(decoder: Decoder)
// 自动生成的init方法调用container
Decoder container(keyedBy:CodingKeys)
// 解析的容器
KeyedDecodingContainer decoderIfPresent(type:Type) or decode(type:Type)
// 内部类,循环调用unbox
_JSONDecoder unbox(value:Any type:Type)
...循环,直到基本类型

最终的解析落到, _JSONDecoder 的 unbox 及 KeyedDecodingContainer 的 decoderIfPresent decode 方法。但 _JSONDecoder 是内部类,我们处理不了。最终决定对 KeyedDecodingContainer 下手,其中部分代码如下:


extension KeyedDecodingContainer {
.......
/// Decode (Int, String) -> Int if possiable
public func decodeIfPresent(_ type: Int.Type, forKey key: K) throws -> Int? {
if let value = try? decode(type, forKey: key) {
return value
}
if let value = try? decode(String.self, forKey: key) {
return Int(value)
}
return nil
}
.......
/// Avoid the failure just when decoding type of Dictionary, Array, SubModel failed
public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T? where T : Decodable {
return try? decode(type, forKey: key)
}
}

上述代码中,第一个函数 decodeIfPresent(_ type: Int.Type, forKey key: K) 是以 key 的信息解析出 Int? 值。这里覆盖了 KeyedDecodingContainer 中的该函数的实现,现在已 try? 的形式以 Int 类型解析,解析成功则直接返回,失败则以 String 类型解析出一个StringValue,如果解析成功,再把 String 转换成 Int? 值。


为什么要写第二个函数呢?


场景:当我们Model内有其他的非基本类型的Model,比如其他自定义Model, Dictionary<String, Any> , Array<String> 等,当这些Model 类型不匹配或者出错误时也会抛出异常,导致整个大Model解析失败。


覆盖 decodeIfPresent<T>(_ type: T.Type, forKey key: K) 可以避免这些场景。至此,当类型过程中出现解析的Optional类型出现不匹配时,我们要不是通过转换,要不就是给其赋值 nil ,避免了系统此时直接 throw exception 导致退出整个解析过程的尴尬。





为何不覆盖 decode 方法? decodeIfPresent 可以返回Optional值, decode 返回确定类型值。考虑到如果Model内如果定义的类型是No-Optional型,那么可以认为开发者确定该值必须存在,如果不存在Model很可能是错误的,所以直接fail。


完整扩展代码点我


最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台