Swift Json实例详细解析
前言
客户端开发项目中,不可避免地需要解析网络数据---将服务端下发的JSON数据解析成客户端可阅读友好的Model。Objective-C下使用最多的是JSONModel,它能在OCRuntime基础下很好地完成解析工作。那么在纯Swift代码中,这个功能是如何实现的?下面开始我们的探索~
- 手动解析
- 原生:Swift4.0JSONDecoder
- JSONDecoder问题及解决方案
手动解析
假设一个User类要解析,Json如下:
{
"userId":1,
"name":"Jack",
"height":1.7,
}
对应的创建一个User结构体(也可以是类):
structUser{
varuserId:Int?
varname:String?
varheight:CGFloat?
}
把JSON转成User
在Swift4.0前,我们以手动解析的方式将JSONmodel化。给User加一个以JSON为参数的初始化方法,代码如下:
structUser{
...
init?(json:[String:Any]){
guardletuserId=json["userId"]as?Int,
letname=json["name"]as?String,
letheight=json["height"]as?CGFloatelse{returnnil}
self.userId=userId
self.name=name
self.height=height
}
}
依次从json中取出model所需的具体类型的数据,填充到具体对应属性中。如果其中一个转换失败或者没有值,初始化会失败返回nil。
如果某个值不需要强校验,直接取值再赋值,把guardlet内的语句去掉。例如,若height不用校验,可看如下代码:
structUser{
...
init?(json:[String:Any]){
guardletuserId=json["userId"]as?Int,
letname=json["name"]as?Stringelse{returnnil}
self.userId=userId
self.name=name
self.height=json["height"]as?CGFloat
}
}
原生:Swift4.0JSONDecoder
2017年6月份左右Swift4.0发布,其中一个重大更新就是JSON的加解密。摆脱手工解析字段的繁琐,聊聊几行代码就可将JSON转换成Model。与Objective-C下的JSONModel极为相似。同样解析上述例子中的User,Swift4.0可以这么写:
structUser:Decodable{
varuserId:Int?
varname:String?
varheight:CGFloat?
}
letdecoder=JSONDecoder()
ifletdata=jsonString.data(using:String.Encoding.utf8){
letuser=try?decoder.decode(User.self,from:data)
}
soeasy~与手动解析不同点在于:
1.移除了手写init?方法。不需要手动解了
2.User实现Decodable协议,协议的定义如下:
///Atypethatcandecodeitselffromanexternalrepresentation.
publicprotocolDecodable{
///Createsanewinstancebydecodingfromthegivendecoder.
///
///Thisinitializerthrowsanerrorifreadingfromthedecoderfails,or
///ifthedatareadiscorruptedorotherwiseinvalid.
///
///-Parameterdecoder:Thedecodertoreaddatafrom.
publicinit(fromdecoder:Decoder)throws
}
Decodable协议只有一个方法publicinit(fromdecoder:Decoder)throws---以Decoder实例进行初始化,初始化失败可能抛出异常。庆幸的是,只要继承Decodable协议,系统会自动检测类中的属性进行初始化工作,省去了人工解析的麻烦~
3.使用了JSONDecoder。它是真正的解析工具,主导整个解析过程
读到这里,是不是觉得人生从黑暗迈向了光明~~
可是,它并不完美...
JSONDecoder问题及方案
解析JSON经常遇到这样两种不一致问题:
- 服务端下发的key跟端上不一致。比如,服务端下发key="order_id",端上定义key="orderId"
- 服务端下发的日期表达是yyyy-MM-ddHH:mm或者时间戳,但端上是Date类型
- 服务端下发的基本类型和端上定义的不一致。服务端下发的是String,端上定义的Int,等
前两个问题JSONDecoder都能很好地解决。
第一个key不一致问题,JSONDecoder有现成的方案。以上面介绍的例子来说,假设服务端返回的key是user_id而不是userId,那么我们可以使用JSONDecoder的CodingKeys像JSONModel一样对属性名称在加解密时的名称做转换。User修改如下:
structUser:Decodable{
varuserId:Int?
varname:String?
varheight:CGFloat?
enumCodingKeys:String,CodingKey{
caseuserId="user_id"
casename
caseheight
}
}
第二个,Date转换问题。JSONDecoder也为我们提供了单独的API:
openclassJSONDecoder{
///Thestrategytousefordecoding`Date`values.
publicenumDateDecodingStrategy{
///Deferto`Date`fordecoding.Thisisthedefaultstrategy.
casedeferredToDate
///Decodethe`Date`asaUNIXtimestampfromaJSONnumber.
casesecondsSince1970
///Decodethe`Date`asUNIXmillisecondtimestampfromaJSONnumber.
casemillisecondsSince1970
///Decodethe`Date`asanISO-8601-formattedstring(inRFC3339format).
caseiso8601
///Decodethe`Date`asastringparsedbythegivenformatter.
caseformatted(DateFormatter)
///Decodethe`Date`asacustomvaluedecodedbythegivenclosure.
casecustom((Decoder)throws->Date)
}
......
///Thestrategytouseindecodingdates.Defaultsto`.deferredToDate`.
openvardateDecodingStrategy:JSONDecoder.DateDecodingStrategy
}
设置好了JSONDecoder属性dateDecodingStrategy后,解析Date类型就会按照指定的策略进行解析。
类型不一致
至此,JSONDecoder为我们提供了
- 解析不同key值对象
- Date类型可自定义转换
- Float在一些正负无穷及无值得特殊表示。(出现的概率很少,不作具体说明了)
但遇到基本类型端上与服务端不一致时(比如一个数字1,端上的Code是Int型,服务端下发String:"1"),JSONDecoder会抛出typeMismatch异常而终结整个数据的解析。
这让人有点懊恼,端上的应用,我们希望它能够尽可能稳定,而不是某些情况下遇到若干个基本类型不一致整个解析就停止,甚至是Crash。
如下面表格所示,我们希望类型不匹配时,能够这么处理:左列代表前端的类型,右列代表服务端类型,每一行代表前端类型为X时,能从服务端下发的哪些类型中转化,比如String可以从IntorFloat转化。这几个类型基本能覆盖日常服务端下发的数据,其它类型的转化可根据自己的需求扩充。
前端 |
服务端 |
|---|---|
| String | Int,Float |
| Float | String |
| Double | String |
| Bool | String,Int |
JSONDecoder没有给我们便利的这种异常处理的API。如何解决呢?最直接的想法,在具体的model内实现init(decoder:Decoder)手动解析可以实现,但每个都这么处理太麻烦。
解决方案:KeyedDecodingContainer方法覆盖
研究JSONDecoder的源码,在解析自定义Model过程中,会发现这样一个调用关系。
//入口方法 JSONDecoderdecoder(type:Typedata:Data) //内部类,真实用来解析的 _JSONDecoderunbox(value:Anytype:Type) //Model调用init方法 Decodableinit(decoder:Decoder) //自动生成的init方法调用container Decodercontainer(keyedBy:CodingKeys) //解析的容器 KeyedDecodingContainerdecoderIfPresent(type:Type)ordecode(type:Type) //内部类,循环调用unbox _JSONDecoderunbox(value:Anytype:Type) ...循环,直到基本类型
最终的解析落到,_JSONDecoder的unbox及KeyedDecodingContainer的decoderIfPresentdecode方法。但_JSONDecoder是内部类,我们处理不了。最终决定对KeyedDecodingContainer下手,其中部分代码如下:
extensionKeyedDecodingContainer{
.......
///Decode(Int,String)->Intifpossiable
publicfuncdecodeIfPresent(_type:Int.Type,forKeykey:K)throws->Int?{
ifletvalue=try?decode(type,forKey:key){
returnvalue
}
ifletvalue=try?decode(String.self,forKey:key){
returnInt(value)
}
returnnil
}
.......
///AvoidthefailurejustwhendecodingtypeofDictionary,Array,SubModelfailed
publicfuncdecodeIfPresent(_type:T.Type,forKeykey:K)throws->T?whereT:Decodable{
returntry?decode(type,forKey:key)
}
}
上述代码中,第一个函数decodeIfPresent(_type:Int.Type,forKeykey:K)是以key的信息解析出Int?值。这里覆盖了KeyedDecodingContainer中的该函数的实现,现在已try?的形式以Int类型解析,解析成功则直接返回,失败则以String类型解析出一个StringValue,如果解析成功,再把String转换成Int?值。
为什么要写第二个函数呢?
场景:当我们Model内有其他的非基本类型的Model,比如其他自定义Model,Dictionary
覆盖decodeIfPresent
为何不覆盖decode方法?decodeIfPresent可以返回Optional值,decode返回确定类型值。考虑到如果Model内如果定义的类型是No-Optional型,那么可以认为开发者确定该值必须存在,如果不存在Model很可能是错误的,所以直接fail。
完整扩展代码点我 (本地下载点我)
总结
Swift4.0JSONDecoder确实为解析数据带来了极大的便利。使用方式上类似Objective-C下的JSONModel。但实际开发中还是需要一些改造才能更好地服务于我们。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。