美文网首页iOSswift计算机技术一锅炖
[HandyJSON] 在Swift语言中处理JSON - 转换

[HandyJSON] 在Swift语言中处理JSON - 转换

作者: xycn | 来源:发表于2016-10-02 14:34 被阅读12410次

    背景

    JSON是移动端开发常用的应用层数据交换协议。最常见的场景便是,客户端向服务端发起网络请求,服务端返回JSON文本,然后客户端解析这个JSON文本,再把对应数据展现到页面上。

    但在编程的时候,处理JSON是一件麻烦事。在不引入任何轮子的情况下,我们通常需要先把JSON转为Dictionary,然后还要记住每个数据对应的Key,用这个Key在Dictionary中取出对应的Value来使用。这个过程我们会犯各种错误:

    • Key拼写错了;
    • 路径写错了;
    • 类型搞错了;
    • 没拿到值懵逼了;
    • 某一天和服务端约定的某个字段变更了,没能更新所有用到它的地方;
    • ...

    为了解决这些问题,很多处理JSON的开源库应运而生。在Swift中,这些开源库主要朝着两个方向努力:

    1. 保持JSON语义,直接解析JSON,但通过封装使调用方式更优雅、更安全;
    2. 预定义Model类,将JSON反序列化为类实例,再使用这些实例;

    对于1,使用最广、评价最好的库非 SwiftyJSON 莫属,它很能代表这个方向的核心。它本质上仍然是根据JSON结构去取值,使用起来顺手、清晰。但也正因如此,这种做法没能妥善解决上述的几个问题,因为Key、路径、类型仍然需要开发者去指定;

    对于2,我个人觉得这是更合理的方式。由于Model类的存在,JSON的解析和使用都受到了定义的约束,只要客户端和服务端约定好了这个Model类,客户端定义后,在业务中使用数据时就可以享受到语法检查、属性预览、属性补全等好处,而且一旦数据定义变更,编译器会强制所有用到的地方都改过来才能编译通过,非常安全。这个方向上,开源库们做的工作,主要就是把JSON文本反序列化到Model类上了。这一类JSON库有 ObjectMapperJSONNeverDie、以及我开发的 HandyJSON 哈哈。

    为什么用HandyJSON

    在Swift中把JSON反序列化到Model类,在HandyJSON出现以前,主要使用两种方式:

    1. 让Model类继承自NSObject,然后class_copyPropertyList()方法获取属性名作为Key,从JSON中取得Value,再通过Objective-C runtime支持的KVC机制为类属性赋值;如JSONNeverDie

    2. 支持纯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字符串
    

    自定义映射和排除

    和反序列化一样,只要定义mappingexclude就可以了。被排除的属性,序列化和反序列化都不再影响到它。而在mapping中定义的Transformer,同时定义了序列化和反序列的规则,所以只要为属性指明一个Transformer关系就可以了。

    总结

    有了HandyJSON的支持,现在我们可以开心地在Swift中使用JSON了。这个库还在更新中,已经支持了Swift 2.2+, Swift 3.0+。如果大家有什么需求或者建议,快去 https://github.com/alibaba/handyjson 给作者(哈哈没错就是我)提issue吧~~

    相关文章

      网友评论

      • 一毫米距离:请问可以把json串转化为数组吗 目前我看到的是转化为model 能不能直接转换为[model] 数组的形式呢
      • 梁森的简书:如何字典转模型呢?
      • KeyboardLife:JSONDeserializer.update(object: &self, from: dic) 在模型初始化的时候字典转模型,这种写法为啥不行呢,为什么不能传入self对象本身
      • 冰三尺:更新了Swift 4.1 和xcode9.3 使用HandyJSON时 直接crash 了, var numberOfFields: Int { return Int(pointer.pointee.numberOfFields) } crash到了这个地方, 请问, 您有关注过这个问题吗?, 有解决办法吗?
      • bc2f84072b7a:您好 请问不同json返回的不同字段 对应本类的同一属性 这时候的mapper应该怎样写呢 谢谢 文档中没有这个体现
      • 初来乍倒:swift4 用不了啊 Integer' was obsoleted in Swift 4
      • junfly:大神... 序列化时 自定义映射 这个mapping怎么写? 和反序列化完全一样么?
      • 丘山Ivan:大神,我在使用HandyJSON时,value 是 大段xml富文本的时候解析失败了。。。能劳烦有时间解答一下下吗?
      • Mars飘殇:你好,我想问下假如我定义了一个对象,里面有个数组属性 var attachments: Array<JBAttachment>!,数组中每个值是JBAttachment对象,然后填充内容后,用toJson()方法转换该对象,里面的数组属性变成了带Optional的值,"attachments": [Optional(["name": 1504489143575.png, "image": <UIImage: 0x1c42bc860>, {800, 800}, "path": 1504489143575.png]), Optional(["name": 1504489143734.png, "image": <UIImage: 0x1c02aa320>, {828, 1472}, "path": 1504489143734.png])],如何去除这些optional呢
      • SADF:厉害,根JSONModel差不多!
      • 纠结的哈士奇:有个小疑问:
        我按文中的例子,有一个var doubleOptional: Double? ,
        然后我定义一个json字符串,其中doubleOptional = 1.1,但转化完成后,我打印出来,变成了Optional(1.1000000000000001) ,好像double类型变成了全部长度,这个总感觉不太好,有其他人遇到没呢?
      • 戎码一生为了谁:查了好多资料没找到解决办法,还是来着提问下把,希望能看到,我有一个比较复杂的json数据,模型对应建立好了,但是就是解析不出来,不知道是不是还不支持我这种,数据格式这样的:{
        "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数组里面的值,请问该怎么弄呢?
        糊涂0:你这种成功了吗??楼主
      • crystalztl:请问如何结合使用core data? 直接使用extension吗?
      • 健健锅:我在model 中重写的某个属性的 set 方法 但是不执行 是什么情况
      • 工匠良辰:不错,满足了我很多的期待。
      • 布袋的世界:太牛B了 解析Json竟然可以这么简洁方便
      • Rayman_智:你好,我现在想在HandyJSON外面在封装一层,需要传一个struct或者class的名称(model.self),我改怎么把参数传到JSONDeserializer<XXX>中啊?请指教~
        爱猫的仙生:同样有这个头痛的问题啊,因为所有的model都需要用到json转模型的方法,于是我就想着抽出来,增加了一个模型的基类,结合swiftyjson,这样调用起来方便一点

        class func setValue(json: Any) -> AnyObject? {
        let jsonString = JSON(json)
        let obj = JSONDeserializer<BaseObject>.deserializeFrom(json: jsonString.rawString())
        return obj
        }

        但是关键就是在这个泛型上面了,貌似这个泛型只认基类的类型,但是不认基类的子类的类型,这就导致子类转模型失败,请问一下这个要怎么做
        xycn:我没太听明白你意思。。JSONDeserializer<XXX>,这里是泛型的用法,XXX要求必须是一个明确的类型,而不能是运行时才决定的内容。但你可以把需求再说明确一些~

        ps. 这里我回复可能不太及时,可以直接在github项目上加issue
      • Rayman_智:功能很全!!!简直无情啊,会用在项目中,期待继续优化升级
      • 6f6bec9c26e6:func toString() -> String? {
        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吗
        6f6bec9c26e6:@坏米饭
        } else if self is NSArray {
        // return "\(self as! NSArray)"
        return JSONSerializer.serialize(array: self as! NSArray).toJSON()
        } else if self is NSDictionary {

        我改了一下
      • butcheryl:我觉得针对于swift 3.0 可以重新根据3.0的命名风格重新设计一下API, 现在的命名有点冗余
      • caafcafc07c9:模型转Json的时候,是不是麻烦了一点?
        xycn:@团长168 请问你是指调用函数写起来麻烦吗?我主要是考虑了
        1. 将Model转换为 JSON串;
        2. 将Model转换为简单字典;
        3. 将 [Model]/[String: Model] 转换为 JSON串;
        4. 将 [Model]/[String: Model] 整体结构转换为简单集合类型;
        这几种情况。API想了很久怎么设计,最后决定用这种方式。最早只支持Model转换为JSON串的时候,API是 `JSONSerializer.serializeToJSON(object: Any) -> String`。
        这里问下你有什么建议么?
      • 5fb37a11a23b:ObjectMapper 之所以很火,1是因为简易的模型转换,2是因为强大并可扩展的Transforms, 3 Alamofire+、Realm、SQL扩展。
        目前你还只做到了1.
        加油~~
        caafcafc07c9:@ftkey bjectMapper的Json转换,好麻烦。。。
      • mieGod:```
        struct MHBanner: HandyJSON{
        var type: String?
        }
        struct MHHomeDataModel : HandyJSON{
        var banner: [MHBanner]?
        }
        ```
        你好,这种怎么转,需要自己写扩展函数吗?
        模型中有一个,模型数组的属性
        xycn:@hwwp 不用的,直接转就可以了~ 参考:https://github.com/alibaba/handyjson#composition-object
      • 刘书亚的天堂之路:关于:嵌套的Model类 和前面复杂数据类型,我看到作者反复听到类的概念,但是明明定义的是struct? 麻烦解释一下,是我理解有误吗? 结构体和类应该不一样吧,为什么作者反复用结构体却又提类
        刘书亚的天堂之路:@阿里学院 😄😄😄哪里话,我只是为了自己理解,所以求证下
        xycn:@阿里学院 非常抱歉,我这样描述确实不够严谨。在描述这个库的能力时,我是想要表达将JSON文本根据“数据结构定义”反序列化为“数据结构实例”。Swift中用以描述“数据结构定义”的方式有class和struct,为了表现HandyJSON除了class以外还支持struct,我偷懒,就把两者混起来写了。
        多谢您指出了这个错误!我会更新文档,把两者分开。
      • 839423639c61:报错阿
        xycn:@839423639c61 可以到项目页面提issue~我们会处理的
      • Frankxp:目测满足日常项目各种解析需求,兼顾很全,赞个。
      • cc6f044cf6dd:超级好用啊
      • 酷走天涯:不错,干的好,分享快乐
      • leacode:看完精彩的评论,发表一点意见:
        1、这里的所有属性都用optional type不是最合理的,因为在使用的时候都需要解开,确实性能和易用性上会存在问题,这一点SwiftyJson解决的很好,不妨借鉴一下。
        2、自定义解析方式,我看楼主是用字符串的方式来展现的,其实可以写一个外部通用类,用对象的形式来获取内部所需的部分数据,不仅是代码的可维护性和易用性都会有所提高。
        3、楼主也是注意到了,很多swift库都需要手动进行赋值,写明哪个字段对应的哪个属性,使用oc的runtime虽然不支持optional type,但是性能确实强一点,但是咱们用的是swift,就得用swift的思想来写,除了性能我认为更值得注意的是安全性,所以不妨朝着安全这方面着手,我也相信Swift写出来的东西最终会比oc强大,但首先要做到不要放弃,不断改进。
        xycn:@leacode 谢谢你提出的意见。我是这么想的:
        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`实现另一种做法,也是给大家多一个选择。在使用中他,它确实解决了我们项目的痛点,感觉还是有用的。
        谢谢你的建议和鼓励~
      • 子达如何:期待原理分享
        xycn:@子达如何 @冰琳92 http://www.jianshu.com/p/eac4a92b44ef 不好意思啊,拖到今天才写出了这一篇简析。。
        冰琳92:思路很赞,同样期待原理分析
        xycn:@子达如何 哈哈,我尽快补上~

      本文标题:[HandyJSON] 在Swift语言中处理JSON - 转换

      本文链接:https://www.haomeiwen.com/subject/zeddyttx.html