客户端开发项目中,不可避免地须要解析网络数据---将服务端下发的JSON数据解析成客户端可阅读友好的Model。Objective-C下使用最多的是JSONModel
,它能在OC Runtime基础下很好地完成解析工做。那么在纯Swift代码中,这个功能是如何实现的?下面开始咱们的探索~前端
JSONDecoder
问题 及 解决方案假设一个User类要解析,Json以下:git
{
"userId": 1,
"name": "Jack",
"height": 1.7,
}
复制代码
对应的建立一个User结构体(也能够是类):github
struct User {
var userId: Int?
var name: String?
var height: CGFloat?
}
复制代码
在Swift4.0前,咱们以手动解析的方式将JSON model化。给User加一个以JSON为参数的初始化方法,代码以下: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。swift
若是某个值不须要强校验,直接取值再赋值,把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
}
}
复制代码
2017年6月份左右Swift4.0发布,其中一个重大更新就是JSON的加解密。摆脱手工解析字段的繁琐,聊聊几行代码就可将JSON转换成Model。与Objective-C下的JSONModel
极为类似。一样解析上述例子中的User,Swift4.0能够这么写:app
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
协议,协议的定义以下:spa
/// 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
。它是真正的解析工具,主导整个解析过程
读到这里,是否是以为人生从黑暗迈向了光明~~
但是,它并不完美...
解析JSON常常遇到这样两种不一致问题:
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
orFloat
转化。这几个类型基本能覆盖平常服务端下发的数据,其它类型的转化可根据本身的需求扩充。
前端 | 服务端 |
---|---|
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。
Swift4.0 JSONDecoder
确实为解析数据带来了极大的便利。使用方式上相似Objective-C下的JSONModel
。但实际开发中仍是须要一些改造才能更好地服务于咱们。
对文章有任何疑问或者有想探讨的问题,随时留言沟通~