美文网首页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吧~~

相关文章

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

    背景 JSON是移动端开发常用的应用层数据交换协议。最常见的场景便是,客户端向服务端发起网络请求,服务端返回JSO...

  • swift 数据解析

    Swift:分别使用SwiftyJSON、ObjectMapper、HandyJSON处理JSON[https:/...

  • Alamofire + HandyJSON 封装

    HandyJSON HandyJSON 是一个用于 Swift 语言中的 JSON 序列化/反序列化库。 与其他流...

  • 常用的第三方

    一、HandyJSON 1、简要说明 HandyJSON是一个用于Swift语言中的JSON序列化/反序列化库。与...

  • HandyJSON

    HandyJSON 中文文档Swift实现JSON转Model - HandyJSON使用讲解

  • Json转模型3--HandyJSON

    HandyJSON阿里巴巴开源的Swift环境下用的Json转模型工具。 为什么用HandyJSON 在Swift...

  • Swift-HandyJSON的使用

    HandyJson 是阿里巴巴开源的一个用于Swift语言中的JSON序列化/反序列化库,可以很方便进行json与...

  • 三方库选型

    一. 数据解析库: SwiftyJson(网络请求的Json串转换,便于做验证数据的处理),HandyJson(完...

  • HandyJSON简单使用

    一、HandyJSON简介 HandyJSON 是阿里开发的一个在swift上把JSON数据转化为对应model的...

  • Swift-转模型HandyJSON

    一 简介二 特性三 安装使用以及封装四 使用示例 一 简介 HandyJSON是一个用于Swift语言中的JSON...

网友评论

  • 一毫米距离:请问可以把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