背景
JSON是移动端开发常用的应用层数据交换协议。最常见的场景便是,客户端向服务端发起网络请求,服务端返回JSON文本,然后客户端解析这个JSON文本,再把对应数据展现到页面上。
但在编程的时候,处理JSON是一件麻烦事。在不引入任何轮子的情况下,我们通常需要先把JSON转为Dictionary,然后还要记住每个数据对应的Key,用这个Key在Dictionary中取出对应的Value来使用。这个过程我们会犯各种错误:
- Key拼写错了;
- 路径写错了;
- 类型搞错了;
- 没拿到值懵逼了;
- 某一天和服务端约定的某个字段变更了,没能更新所有用到它的地方;
- ...
为了解决这些问题,很多处理JSON的开源库应运而生。在Swift中,这些开源库主要朝着两个方向努力:
- 保持JSON语义,直接解析JSON,但通过封装使调用方式更优雅、更安全;
- 预定义Model类,将JSON反序列化为类实例,再使用这些实例;
对于1,使用最广、评价最好的库非 SwiftyJSON 莫属,它很能代表这个方向的核心。它本质上仍然是根据JSON结构去取值,使用起来顺手、清晰。但也正因如此,这种做法没能妥善解决上述的几个问题,因为Key、路径、类型仍然需要开发者去指定;
对于2,我个人觉得这是更合理的方式。由于Model类的存在,JSON的解析和使用都受到了定义的约束,只要客户端和服务端约定好了这个Model类,客户端定义后,在业务中使用数据时就可以享受到语法检查、属性预览、属性补全等好处,而且一旦数据定义变更,编译器会强制所有用到的地方都改过来才能编译通过,非常安全。这个方向上,开源库们做的工作,主要就是把JSON文本反序列化到Model类上了。这一类JSON库有 ObjectMapper、JSONNeverDie、以及我开发的 HandyJSON 哈哈。
为什么用HandyJSON
在Swift中把JSON反序列化到Model类,在HandyJSON
出现以前,主要使用两种方式:
-
让Model类继承自
NSObject
,然后class_copyPropertyList()
方法获取属性名作为Key,从JSON中取得Value,再通过Objective-C runtime
支持的KVC
机制为类属性赋值;如JSONNeverDie
; -
支持纯Swift类,但要求开发者实现
Mapping
函数,使用重载的运算符进行赋值,如ObjectMapper
;
这两者都有显而易见的缺点。前者要求Model继承自NSObject
,非常不优雅,且直接否定了用struct
来定义Model的方式;后者的Mapping
函数要求开发者自定义,在其中指明每个属性对应的JSON字段名,代码侵入大,且仍然容易发生拼写错误、维护困难等问题。
而HandyJSON
另辟蹊径,采用Swift反射
+内存赋值
的方式来构造Model实例,规避了上述两个方案遇到的问题,保持原汁原味的Swift类定义。贴一段很能展示这个特点的代码:
// 假设这是服务端返回的统一定义的response格式
class BaseResponse<T: HandyJSON>: HandyJSON {
var code: Int? // 服务端返回码
var data: T? // 具体的data的格式和业务相关,故用泛型定义
public required init() {}
}
// 假设这是某一个业务具体的数据格式定义
struct SampleData: HandyJSON {
var id: Int?
}
let sample = SampleData(id: 2)
let resp = BaseResponse<SampleData>()
resp.code = 200
resp.data = sample
let jsonString = resp.toJSONString()! // 从对象实例转换到JSON字符串
print(jsonString) // print: {"code":200,"data":{"id":2}}
if let mappedObject = JSONDeserializer<BaseResponse<SampleData>>.deserializeFrom(json: jsonString) { // 从字符串转换为对象实例
print(mappedObject.data?.id)
}
如果是继承NSObject
类的话,Model定义是没法用泛型的。
把JSON转换为Model
简单类型
某个Model类想支持通过HandyJSON
来反序列化,只需要在定义时,实现HandyJSON
协议,这个协议只要求实现一个空的init()
函数。
class BasicTypes: HandyJSON {
var int: Int = 2
var doubleOptional: Double?
var stringImplicitlyUnwrapped: String!
required init() {}
}
然后假设我们从服务端拿到这样一个JSON文本:
let jsonString = "{\"doubleOptional\":1.1,\"stringImplicitlyUnwrapped\":\"hello\",\"int\":1}"
引入HandyJSON
以后,我们就可以这样来做反序列化了:
if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
// …
}
简单吧~
支持Struct
如果Model的定义是struct,由于Swift中struct提供了默认构造函数,所以就不需要再实现空的init()
函数了。但需要注意,如果你为strcut指定了别的构造函数,那么就需要保留一个空的实现。
struct BasicTypes: HandyJSON {
var int: Int = 2
var doubleOptional: Double?
var stringImplicitlyUnwrapped: String!
}
let jsonString = "{\"doubleOptional\":1.1,\"stringImplicitlyUnwrapped\":\"hello\",\"int\":1}"
if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
// …
}
支持枚举
支持值类型的enum,只需要声明服从HandyJSONEnum
协议。不再需要其他特殊处理了。
enum AnimalType: String, HandyJSONEnum {
case Cat = "cat"
case Dog = "dog"
case Bird = "bird"
}
struct Animal: HandyJSON {
var name: String?
var type: AnimalType?
}
let jsonString = "{\"type\":\"cat\",\"name\":\"Tom\"}"
if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
print(animal.type?.rawValue)
}
较复杂的类型,如可选、隐式解包可选、集合等
HandyJSON支持这些非基础类型,包括嵌套结构。
class BasicTypes: HandyJSON {
var bool: Bool = true
var intOptional: Int?
var doubleImplicitlyUnwrapped: Double!
var anyObjectOptional: Any?
var arrayInt: Array<Int> = []
var arrayStringOptional: Array<String>?
var setInt: Set<Int>?
var dictAnyObject: Dictionary<String, Any> = [:]
var nsNumber = 2
var nsString: NSString?
required init() {}
}
let object = BasicTypes()
object.intOptional = 1
object.doubleImplicitlyUnwrapped = 1.1
object.anyObjectOptional = "StringValue"
object.arrayInt = [1, 2]
object.arrayStringOptional = ["a", "b"]
object.setInt = [1, 2]
object.dictAnyObject = ["key1": 1, "key2": "stringValue"]
object.nsNumber = 2
object.nsString = "nsStringValue"
let jsonString = object.toJSONString()!
if let object = JSONDeserializer<BasicTypes>.deserializeFrom(json: jsonString) {
// ...
}
指定解析路径
HandyJSON支持指定从哪个具体路径开始解析,反序列化到Model。
class Cat: HandyJSON {
var id: Int64!
var name: String!
required init() {}
}
let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}"
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
print(cat.name)
}
有继承关系的Model类
如果某个Model类继承自另一个Model
类,只需要这个父Model类实现HandyJSON
协议就可以:
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<Cat>.deserializeFrom(json: jsonString) {
print(cat)
}
自定义解析方式
HandyJSON支持自定义映射关系,或者自定义解析过程。你需要实现一个可选的mapping
函数,在里边实现NSString
值(HandyJSON会把对应的JSON字段转换为NSString)转换为你需要的字段类型。
所以无法直接支持的类型,都可以用这个方式支持。
class Cat: HandyJSON {
var id: Int64!
var name: String!
var parent: (String, String)?
required init() {}
func mapping(mapper: HelpingMapper) {
// specify 'cat_id' field in json map to 'id' property in object
mapper <<<
self.id <-- "cat_id"
// specify 'parent' field in json parse as following to 'parent' property in object
mapper <<<
self.parent <-- TransformOf<(String, String), String>(fromJSON: { (rawString) -> (String, String)? in
if let parentNames = rawString?.characters.split(separator: "/").map(String.init) {
return (parentNames[0], parentNames[1])
}
return nil
}, toJSON: { (tuple) -> String? in
if let _tuple = tuple {
return "\(_tuple.0)/\(_tuple.1)"
}
return nil
})
}
}
let jsonString = "{\"cat_id\":12345,\"name\":\"Kitty\",\"parent\":\"Tom/Lily\"}"
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
print(cat.id)
print(cat.parent)
}
排除指定属性
如果在Model中存在因为某些原因不能实现HandyJSON
协议的非基本字段,或者不能实现HandyJSONEnum
协议的枚举字段,又或者说不希望反序列化影响某个字段,可以在mapping
函数中将它排除。如果不这么做,可能会出现未定义的行为。
class NotHandyJSONType {
var dummy: String?
}
class Cat: HandyJSON {
var id: Int64!
var name: String!
var notHandyJSONTypeProperty: NotHandyJSONType?
var basicTypeButNotWantedProperty: String?
required init() {}
func mapping(mapper: HelpingMapper) {
mapper >>> self.notHandyJSONTypeProperty
mapper >>> self.basicTypeButNotWantedProperty
}
}
let jsonString = "{\"name\":\"cat\",\"id\":\"12345\"}"
if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
print(cat)
}
把Model转换为JSON文本
HandyJSON
还支持对象到字典、到JSON字符串的序列化功能。
基本类型
现在,序列化也要求Model声明服从HandyJSON
协议。
class BasicTypes: HandyJSON {
var int: Int = 2
var doubleOptional: Double?
var stringImplicitlyUnwrapped: String!
required init() {}
}
let object = BasicTypes()
object.int = 1
object.doubleOptional = 1.1
object.stringImplicitlyUnwrapped = “hello"
print(object.toJSON()!) // 序列化到字典
print(object.toJSONString()!) // 序列化到JSON字符串
print(object.toJSONString(prettyPrint: true)!) // 序列化为格式化后的JSON字符串
自定义映射和排除
和反序列化一样,只要定义mapping
和exclude
就可以了。被排除的属性,序列化和反序列化都不再影响到它。而在mapping
中定义的Transformer
,同时定义了序列化和反序列的规则,所以只要为属性指明一个Transformer
关系就可以了。
总结
有了HandyJSON
的支持,现在我们可以开心地在Swift中使用JSON了。这个库还在更新中,已经支持了Swift 2.2+, Swift 3.0+。如果大家有什么需求或者建议,快去 https://github.com/alibaba/handyjson 给作者(哈哈没错就是我)提issue吧~~
网友评论
我按文中的例子,有一个var doubleOptional: Double? ,
然后我定义一个json字符串,其中doubleOptional = 1.1,但转化完成后,我打印出来,变成了Optional(1.1000000000000001) ,好像double类型变成了全部长度,这个总感觉不太好,有其他人遇到没呢?
"bankTree" : {
"zhiHangList" : [
{
"zhiHang" : {
"bankOrgId" : "6F1E247962434A2790E205C819CD91C4",
"parentNo" : 103,
"identityNo" : 104,
"name" : "宁波银行下关支行",
"role" : "zh"
},
"renyuanList" : [
{
"bankOrgId" : "59083B3825A344848A70CA3F34648C88",
"parentNo" : 104,
"identityNo" : 106,
"name" : "renxuan(主管)",
"role" : "zg"
}
]
},
{
"zhiHang" : {
"bankOrgId" : "6F74C75E9CC841E39C149F9E86E7E300",
"parentNo" : 103,
"identityNo" : 105,
"name" : "宁波银行建康支行",
"role" : "zh"
},
"renyuanList" : [
{
"bankOrgId" : "C36DEBA6987349CC9FBD6A73672D2208",
"parentNo" : 105,
"identityNo" : 156,
"name" : "王台(经理)",
"role" : "jl"
}
]
}
]
}
}
我要拿到zhiHang里面的那几个字段和renyuanList数组里面的值,请问该怎么弄呢?
class func setValue(json: Any) -> AnyObject? {
let jsonString = JSON(json)
let obj = JSONDeserializer<BaseObject>.deserializeFrom(json: jsonString.rawString())
return obj
}
但是关键就是在这个泛型上面了,貌似这个泛型只认基类的类型,但是不认基类的子类的类型,这就导致子类转模型失败,请问一下这个要怎么做
ps. 这里我回复可能不太及时,可以直接在github项目上加issue
if self is NSString {
return self as? String
} else if self is NSNumber {
// Boolean Type Inside
if NSStringFromClass(type(of: self)) == "__NSCFBoolean" {
if (self as! NSNumber).boolValue {
return "true"
} else {
return "false"
}
}
return (self as! NSNumber).stringValue
} else if self is NSArray {
return "\(self as! NSArray)"
} else if self is NSDictionary {
return "\(self as! NSDictionary)"
}
return nil
}
这里用"\(self as! NSArray)"把nsarray实例转换成的字符串,我还能再给转换成nsarray吗
} else if self is NSArray {
// return "\(self as! NSArray)"
return JSONSerializer.serialize(array: self as! NSArray).toJSON()
} else if self is NSDictionary {
我改了一下
1. 将Model转换为 JSON串;
2. 将Model转换为简单字典;
3. 将 [Model]/[String: Model] 转换为 JSON串;
4. 将 [Model]/[String: Model] 整体结构转换为简单集合类型;
这几种情况。API想了很久怎么设计,最后决定用这种方式。最早只支持Model转换为JSON串的时候,API是 `JSONSerializer.serializeToJSON(object: Any) -> String`。
这里问下你有什么建议么?
目前你还只做到了1.
加油~~
struct MHBanner: HandyJSON{
var type: String?
}
struct MHHomeDataModel : HandyJSON{
var banner: [MHBanner]?
}
```
你好,这种怎么转,需要自己写扩展函数吗?
模型中有一个,模型数组的属性
多谢您指出了这个错误!我会更新文档,把两者分开。
1、这里的所有属性都用optional type不是最合理的,因为在使用的时候都需要解开,确实性能和易用性上会存在问题,这一点SwiftyJson解决的很好,不妨借鉴一下。
2、自定义解析方式,我看楼主是用字符串的方式来展现的,其实可以写一个外部通用类,用对象的形式来获取内部所需的部分数据,不仅是代码的可维护性和易用性都会有所提高。
3、楼主也是注意到了,很多swift库都需要手动进行赋值,写明哪个字段对应的哪个属性,使用oc的runtime虽然不支持optional type,但是性能确实强一点,但是咱们用的是swift,就得用swift的思想来写,除了性能我认为更值得注意的是安全性,所以不妨朝着安全这方面着手,我也相信Swift写出来的东西最终会比oc强大,但首先要做到不要放弃,不断改进。
1. 刚接触swift时,可选确实很遭抵触。我所在项目去年十一月份开始全面迁移swift,将近一年的使用后,我们都认可了可选是swift的一大利器: 它会让你在写代码时一直考虑手中的变量会不会为空值。我们觉得这更安全。`SwiftyJSON`其实拿到的也是可选,如项目readme中的sample:
```
let json = JSON(data: dataFromNetworking)
if let userName = json[0]["user"]["name"].string {
//Now you got your value
}
```
所以我个人觉得可选的支持是很有必要的。
2. 这里我没太听明白。。。先表明下我的思路是:我是在使用`ObjectMapper`库的过程中设计、实现`HandyJSON`的,所以在一些地方参考了它的设计。可以在 https://github.com/Hearst-DD/ObjectMapper#custom-transforms 这里看到,它的自定义解析就是让开发者自己定义`String`到特定类型的转换。我觉得这也是比较自然的,因为`JSON`本身就是用字符串表达。
3. 这一点说得很对,Swift很重视安全性。`ObjectMapper`、`SwiftyJSON`等库的用法很`Swift`,都很优秀。我依赖Swift的能力而非`objc-runtime`实现另一种做法,也是给大家多一个选择。在使用中他,它确实解决了我们项目的痛点,感觉还是有用的。
谢谢你的建议和鼓励~