Json转模型3--HandyJSON

作者: JoeXP | 来源:发表于2017-05-02 11:34 被阅读288次

    HandyJSON阿里巴巴开源的Swift环境下用的Json转模型工具。

    为什么用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实例,规避了上述两个方案遇到的问题。

    JSON转为Model

    简单类型

    某个Model类想支持通过HandyJSON来反序列化,只需要在定义时,实现HandyJSON协议,这个协议只要求实现一个空的init()函数。
    比如我们和服务端约定了一个Animal数据,里面有name/id/num字段,那么我们这样定义Animal类:

    class Animal: HandyJSON {
        var name: String?
        var id: String?
        var num: Int?
    
        required init() {} // 如果定义是struct,连init()函数都不用声明;
    }
    

    然后假设我们从服务端拿到这样一个JSON文本:

    let jsonString = "{\"name\":\"cat\",\"id\":\"12345\",\"num\":180}"
    

    引入HandyJSON以后,我们就可以这样来做反序列化了:

    if let animal = JSONDeserializer<Animal>.deserializeFrom(json: jsonString) {
        print(animal.name)
        print(animal.id)
        print(animal.num)
    }
    
    比较复杂的类型

    HandyJSON支持在类定义里使用各种形式的基本属性,包括可选(?),隐式解包可选(!),数组(Array),字典(Dictionary),Objective-C基本类型(NSString、NSNumber),各种类型的嵌套([Int]?、[String]?、[Int]!、...)等等。比如下面这个看起来比较复杂的类型:

    struct Cat: HandyJSON {
        var id: Int64!
        var name: String!
        var friend: [String]?
        var weight: Double?
        var alive: Bool = true
        var color: NSString?
    }
    

    一样轻松转换:

    let jsonString = "{\"id\":1234567,\"name\":\"Kitty\",\"friend\":[\"Tom\",\"Jack\",\"Lily\",\"Black\"],\"weight\":15.34,\"alive\":false,\"color\":\"white\"}"
    
    if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString) {
        print(cat.xxx)
    }
    
    嵌套的Model类

    如果Model类中的某个属性是另一个自定义的Model类,那么只要那个Model类也实现了HandyJSON协议,就一样可以转换:

    struct Component: HandyJSON {
        var aInt: Int?
        var aString: String?
    }
    
    struct Composition: HandyJSON {
        var aInt: Int?
        var comp1: Component?
        var comp2: Component?
    }
    
    let jsonString = "{\"num\":12345,\"comp1\":{\"aInt\":1,\"aString\":\"aaaaa\"},\"comp2\":{\"aInt\":2,\"aString\":\"bbbbb\"}}"
    
    if let composition = JSONDeserializer<Composition>.deserializeFrom(json: jsonString) {
        print(composition)
    }
    
    有继承关系的类型

    如果某个Model类继承自另一个Model类,只需要这个父Model类实现HandyJSON协议就可以

    自定义解析方式

    HandyJSON还提供了一个扩展能力,就是允许自行定义Model类某个字段的解析Key、解析方式。我们经常会有这样的需求:

    1. 某个Model中,我们不想使用和服务端约定的key作为属性名,想自己定一个;
    2. 有些类型如enum、tuple是无法直接从JSON中解析出来的,但我们在Model类中有这样的属性;
    

    HandyJSON协议提供了一个可选的mapping()函数,我们可以在其中指定某个字段用什么Key、或者用什么方法从JSON中解析出它的值。如我们有一个Model类和一个服务端返回的JSON串:

    class Cat: HandyJSON {
        var id: Int64!
        var name: String!
        var parent: (String, String)?
        required init() {}
    }
    let jsonString = "{\"cat_id\":12345,\"name\":\"Kitty\",\"parent\":\"Tom/Lily\"}"
    

    可以看到,Cat类的id属性和JSON文本中的Key是对应不上的;而对于parent这个属性来说,它是一个元组,做不到从JSON中的"Tom/Lily"解析出来。所以我们要定义一个Mapping函数来做这两个支持:

    class Cat: HandyJSON {
        var id: Int64!
        var name: String!
        var parent: (String, String)?
    
        required init() {}
    
        func mapping(mapper: HelpingMapper) {
            // 指定 id 字段用 "cat_id" 去解析
            mapper.specify(property: &id, name: "cat_id")
    
            // 指定 parent 字段用这个方法去解析
            mapper.specify(property: &parent) { (rawString) -> (String, String) in
                let parentNames = rawString.characters.split{$0 == "/"}.map(String.init)
                return (parentNames[0], parentNames[1])
            }
        }
    }
    
    指定反序列化JSON中某个节点

    有时候服务端返回给我们的JSON文本包含了大量的状态信息,和Model无关,比如statusCode,debugMessage等,或者有用的数据是在某个节点以下,那么我们可以指定反序列化哪个节点:

    struct Cat: HandyJSON {
        var id: Int64!
        var name: String!
    }
    
    // 服务端返回了这个JSON,我们想解析的只有data里的cat
    let jsonString = "{\"code\":200,\"msg\":\"success\",\"data\":{\"cat\":{\"id\":12345,\"name\":\"Kitty\"}}}"
    
    // 那么,我们指定解析 "data.cat",通过点来表达路径
    if let cat = JSONDeserializer.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
        print(cat.name)
    }
    

    实战

    先来看看HandyJSON的几个JSON转模型的序列化方法:

    JSONDeserializer<XXXModel>.deserializeFrom(dict: NSDictionary?)
    JSONDeserializer<XXXModel>.deserializeFrom(json: String?)
    JSONDeserializer<XXXModel>.deserializeModelArrayFrom(json: String?)
    JSONDeserializer<XXXModel>.deserializeFrom(dict: NSDictionary?, designatedPath: String?)
    JSONDeserializer<XXXModel>.deserializeFrom(json: String?, designatedPath: String?)
    JSONDeserializer<XXXModel>.deserializeModelArrayFrom(json: String?, designatedPath: String?)
    

    再来看看我们的模拟数据,并创建模型

    let baseInfo: NSDictionary = ["build_name":"置信·原墅",
                                        "build_address":"学院中路与金桥路交汇处东北侧",
                                        "area_address":"浙江省温州市鹿城区五马街道",
                                        "build_num": "12",
                                        "room_num": 588,
                                        "detail_address":["province":"浙江省",
                                                          "city":"温州市",
                                                          "district":"鹿城区",
                                                          "street":"五马街道"]]
    
    import HandyJSON
    
    struct HouseInfo: HandyJSON {
        var build_name: String?
        var build_address: String?
        var build_num: Int?
        var room_num: Int?
        var area_address: String?
        var detail_address: LvFourAddressModel?
    }
    
    struct LvFourAddressModel: HandyJSON {
        var province: String?
        var city: String?
        var district: String?
        var street: String?
    }
    

    我们选择相应的方法进行序列号得到:

    let baseInfo = JSONDeserializer<HouseInfo>.deserializeFrom(dict: baseInfo)
    print(baseInfo?.build_num)
    // Optional(12), 服务器返回的是字符串,但是我们定义的是Int类型,得到了Int, nice
    print(baseInfo?.area_address)
    // Optional("浙江省温州市鹿城区五马街道"), 如果将area_address 定义为Int,那么得到nil
    
    // 如果修改 模型中 build_name 为 name , 那么打印 name 将会因为没有得到对应字段的值打印 nil,
    

    把Model转换为JSON文本

    基本类型

    如果只需要进行序列化,那么在定义Model类时,不需要做任何特殊的改动。任何一个类的实例,直接调用HandyJSON的序列化方法去序列化,就能得到JSON字符串了。

    class Animal {
        var name: String?
        var height: Int?
    
        init(name: String, height: Int) {
            self.name = name
            self.height = height
        }
    }
    
    let cat = Animal(name: "cat", height: 30)
    print(JSONSerializer.serializeToJSON(object: cat)!)
    print(JSONSerializer.serializeToJSON(object: cat, prettify: true)!)
    

    可以通过prettify参数来指定获得的是否是格式化后的JSON串

    复杂类型

    即使Model类中有别的Model类啥的,都一样支持

    总结

    swift语言崇尚简便快速的写法,那么相比较之前讨论的SwiftyJSON,objectMapper,对于我这种初学者,更倾向于先用HandyJSON,作为项目中的JSON-Model互转的工具,且对于小项目绝对够了,等往后遇到什么坑再解决吧。等项目越来越大,接触到各种问题时,再回过头来看看如何解决,哪种工具更适合。

    相关文章

      网友评论

      • Ko_Neko:博主你好,我想问一下,我使用的是Alamofire,返回的data不是标准json字符串 ,有可能是字典或者数组(使用POSTMAN是返回的JSON,但是不知道为什么用alamofire返回后就自动给我转成字典/数组了。。) 有没有一个通用办法把这个返回值再转换回json字符串?
        KeyboardLife:HandyJson如何在模型初始化的时候字典转模型呢 类似于yymodel的 self.yy_modelSet(with: dic)
        lanmoyingsheng::persevere: 坑踩多了就知道为什么了

      本文标题:Json转模型3--HandyJSON

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