HandyJSON库简介

7,276 阅读12分钟

背景

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

但在编程的时候,处理JSON是一件麻烦事。在iOS开发中,在不引入任何轮子的情况下,通常需要先把JSON转为Dictionary,然后还要记住每个数据对应的Key,用这个Key在Dictionary中取出对应的Value来使用。而在手动解析的过程中,经常会犯很多的低价错误,比如Key拼写错误,类型错误,key的空值判断等。

为了解决这些问题,很多处理JSON的开源库应运而生。通过对比可以发现,这些开源库基本都需要具有两个主要的功能:

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

其实,上面所说的两点也是移动开发中JSON解析框架必须具备的两点功能。而具备上面两点的第三方库通常有SwiftyJSON、ObjectMapper、JSONNeverDie、HandyJSON 等,而我们今天要讲的HandyJSON 是一款在Swift开发中使用的比较多的进行Model和JSON间的互相转换的开源库,该库由阿里巴巴技术团队研发,,已经过了大量的实战积累。

HandyJSON的优势

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

  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实例,规避了上述两个方案遇到的问题。不过HandyJSON也并非完美无缺,如经常造成的内存泄露,兼容性差等问题。

HandyJSON使用

HandyJSON需要以下本地环境具备以下条件:

  • iOS 8.0+/OSX 10.9+/watchOS 2.0+/tvOS 9.0+
  • Swift 3.0+ / Swift 4.0+

同时,针对不同的IDE环境和Swift版本,HandyJSON的版本也不一样,可以参考下表。

Xcode Swift HandyJSON
Xcode 10 Swift 4.2 4.2.0
Xcode 9.4.1以下 Swift 4 >= 4.1.1
Xcode 8.3以上 Swift 3.x >= 1.8.0

HandyJSON安装

对于第三方库,一般有两种依赖方式,一种是framwork依赖,一种是源码依赖。使用Cocoapods安装依赖的配置脚本如下:

pod 'HandyJSON', '~> 4.2.0'

然后再执行“pod install”命令来按照HandyJSON库。当然,我们还可以使用Carthage来管理第三方框架和依赖。

github "alibaba/HandyJSON" ~> 4.2.0

当然,我们还可以将HandyJSON库下载下来再使用源码的方式依赖,下面的地址为:github.com/alibaba/Han…

JSON转Model

基础类型

假设我们从服务端拿到JSON文本是这样的:

{
	"name": "cat",
	"id": "12345",
	"num": 180
}

此时,如果我们想要使用HandyJSON来反序列化,只需要定义如下一个Model类即可。

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?
}

假如后台返回的数据内容如下:

{
	"id": 1234567,
	"name": "Kitty",
	"friend": ["Tom", "Jack", "Lily", "Black"],
	"weight": 15.34,
	"alive": false,
	"color": "white"
}

如果要将上面的JSON数据转换为上面定义的Model类,只需要一句话即可。

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)
}

指定JSON中某个节点

有时候服务端返回给我们的JSON文本包含了大量的状态信息,比如statusCode,debugMessage等,这些信息通常和Model是无关的。或者说,我们想要解析JSON中的某个指定节点的数据,对于这种情况,HandyJSON也是支持的。

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<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还支持某些方面的自定义扩展,也即是说HandyJSON允许自行定义Model类某个字段的解析Key、解析方式。或许,在JSON解析中,你经常会碰到下面这样的场景:

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

对于这些情况,我们可以根据HandyJSON协议提供的mapping()函数来实现自定义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函数来自定义支持。此时,Model类如下:

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])
        }
    }
}

Model转JSON

对于Model转JSON,则相对简单,和Android开发中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,例如Model嵌套Model的情况,我们也可以HandyJSON的序列化函数来完成序列化。

enum Gender: String {
    case Male = "male"
    case Female = "Female"
}

struct Subject {
    var id: Int64?
    var name: String?

    init(id: Int64, name: String) {
        self.id = id
        self.name = name
    }
}

class Student {
    var name: String?
    var gender: Gender?
    var subjects: [Subject]?
}

let student = Student()
student.name = "Jack"
student.gender = .Female
student.subjects = [Subject(id: 1, name: "math"), Subject(id: 2, name: "English"), Subject(id: 3, name: "Philosophy")]

print(JSONSerializer.serializeToJSON(object: student)!)
print(JSONSerializer.serializeToJSON(object: student, prettify: true)!)

Codable

Codable 简介

在WWDC2017大会上, Swift4.0的发布新增了一个重要的功能:Codable。Codable是一个协议,其作用类似于NSPropertyListSerialization 和 NSJSONSerialization,主要用于完成 JSON 和Model之间的转换。例如:

typealias Codable = Decodable & Encodable

public protocol Decodable {
    public init(from decoder: Decoder) throws
}
public protocol Encodable {
    public func encode(to encoder: Encoder) throws
}

关于Codable更多的知识可以参考官方的文档介绍。由上面的例子可以发现,Codable并不少单独存在的,它其实是Decodable 和 Encodable的融合。

编码器与解码器

Encoder 和 Decoder 的基本概念跟 NSCoder 类似,对象接受一个编码器,然后调用自己的方法来完成编码或者解码。NSCoder 的API 是很直接的。NSCoder 有一系列像是 encodeObject:forKey 还有encodeInteger:forKey的方法,对象调用他们来完成具体的编码。

Swift 的 API 就没那么直接了, Encoder 不提供编码方法而是提供容器,由容器来完成编码工作。因为容器这个设计, Encoder 和Decoder 这两个协议就非常实用,只需要少量的信息就可以获取容器的方法。例如,下面是一个封装的类CodableHelper.swift

rotocol Encoder {
  var codingPath: [CodingKey?] { get }
  public var userInfo: [CodingUserInfoKey : Any] { get }

  func container<Key>(keyedBy type: Key.Type)
          -> KeyedEncodingContainer<Key> where Key : CodingKey
  func unkeyedContainer() -> UnkeyedEncodingContainer
  func singleValueContainer() -> SingleValueEncodingContainer
}

protocol Decoder {
  var codingPath: [CodingKey?] { get }
  var userInfo: [CodingUserInfoKey : Any] { get }

  func container<Key>(keyedBy type: Key.Type) throws
          -> KeyedDecodingContainer<Key> where Key : CodingKey
  func unkeyedContainer() throws -> UnkeyedDecodingContainer
  func singleValueContainer() throws -> SingleValueDecodingContainer
}

实例

使用Codable解析JSON主要会用到JSONEncoder和JSONDecoder两个函数,其中JSONEncoder用于编码,JSONDecoder用于解析。

let data = try! JSONEncoder().encode([1: 3])
let dict = try! JSONDecoder().decode([Int: Int].self, from: data)
print(dict)

基本类型

Swift的Enum,Struct和Class等基本类型都支持Codable,下面是一个具体的实例。

enum Level: String, Codable {
    case large
    case medium
    case small
}

struct Location: Codable {
    let latitude: Double
    let longitude: Double
}

// CustomDebugStringConvertible只是为了更好打印
class City: Codable, CustomDebugStringConvertible {
    
    let name: String
    let pop: UInt
    let level: Level
    let location: Location
    
    var debugDescription: String {
        return """
        {
        "name": \(name),
        "pop": \(pop),
        "level": \(level.rawValue),
        "location": {
        "latitude": \(location.latitude),
        "longitude": \(location.longitude)
        }
        }
        """
    }
}

let jsonData = """
        {
        "name": "Shanghai",
        "pop": 21000000,
        "level": "large",
        "location": {
          "latitude": 30.40,
          "longitude": 120.51
        }
        }
        """.data(using: .utf8)!
do {
    let city = try JSONDecoder().decode(City.self, from: jsonData)
    print("city:", city)
} catch {
    print(error.localizedDescription)
}

上述实例展示了三种基本类型的基本用法,需要注意的是所有存储属性的类型都需遵循Codable才可以推断,计算属性不受此限制。如有存储属性不遵循Codable,需要自行实现本文开头协议中的方法。

自定义key

由于Codable的key是直接用属性名匹配的,所以当key不匹配时需要我们自定义并实现协议方法。比如将上述的的name字段变成了short_name。 此时我们需要这么做:定义一个枚举遵循CodingKey协议且原始值为String,且实现Decodable的协议方法。

let jsonData = """
        {
        "short_name": "Shanghai",  // 这里的key与model不再吻合
        "pop": 21000000,
        "level": "large",
        "location": {
          "latitude": "30.40",
          "longitude": 120.51
        }
        }
        """.data(using: .utf8)!

class City: Codable, CustomDebugStringConvertible {
   //...其余代码与上例一致
    
    enum CodingKeys: String, CodingKey {
        case name = "short_name"
        case pop
        case level
        case location
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        pop = try container.decode(UInt.self, forKey: .pop)
        level = try container.decode(Level.self, forKey: .level)
        location = try container.decode(Location.self, forKey: .location)
    }
}

泛型

如果模型定义的比较好,其实大部分属性是可以复用的,我们可以通过泛型来实现模型的部分复用。

struct Resource<Attributes>: Codable where Attributes: Codable {
    let name: String
    let url: URL
    let attributes: Attributes
}

struct ImageAttributes: Codable {
    let size: CGSize
    let format: String
}

Resource<ImageAttributes>

有时候JSON中的格式并非我们实际所需要的,比如String格式的浮点型数字,我们希望直接在转model时转换为Double类型。那么Codable也是支持这样的操作的。

struct StringToDoubleConverter: Codable { 
    let value: Double?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        value = Double(string)
    }
}

二次封装

虽然使用Codable可以很方便的完成JSON的转换,但是对于我们项目开发来说仍然不够完美,我们可以使用二次封装。

import Foundation


public extension Encodable {
    //对象转json字符串
    public func toJSONString() -> String? {
        guard let data = try? JSONEncoder().encode(self) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
    
    //对象转jsonObject
    public func toJSONObject() -> Any? {
        guard let data = try? JSONEncoder().encode(self) else {
            return nil
        }
        return try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
    }
}


public extension Decodable {
    //json字符串转对象&数组
    public static func decodeJSON(from string: String?, designatedPath: String? = nil) -> Self? {
        
        guard let data = string?.data(using: .utf8),
            let jsonData = getInnerObject(inside: data, by: designatedPath) else {
                return nil
        }
        return try? JSONDecoder().decode(Self.self, from: jsonData)
    }
    
    //jsonObject转换对象或者数组
    public static func decodeJSON(from jsonObject: Any?, designatedPath: String? = nil) -> Self? {
        
        guard let jsonObject = jsonObject,
            JSONSerialization.isValidJSONObject(jsonObject),
            let data = try? JSONSerialization.data(withJSONObject: jsonObject, options: []),
            let jsonData = getInnerObject(inside: data, by: designatedPath)  else {
                return nil
        }
        return try? JSONDecoder().decode(Self.self, from: jsonData)
    }
}


public extension Array where Element: Codable {
    
    public static func decodeJSON(from jsonString: String?, designatedPath: String? = nil) -> [Element?]? {
        guard let data = jsonString?.data(using: .utf8),
            let jsonData = getInnerObject(inside: data, by: designatedPath),
            let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as? [Any] else {
            return nil
        }
        return Array.decodeJSON(from: jsonObject)
    }
    
    public static func decodeJSON(from array: [Any]?) -> [Element?]? {
        return array?.map({ (item) -> Element? in
            return Element.decodeJSON(from: item)
        })
    }
}


//根据designatedPath获取object中数据
fileprivate func getInnerObject(inside jsonData: Data?, by designatedPath: String?) -> Data? {
    guard let _jsonData = jsonData,
        let paths = designatedPath?.components(separatedBy: "."),
        paths.count > 0 else {
        return jsonData
    }
    //从jsonObject中取出designatedPath指定的jsonObject
    let jsonObject = try? JSONSerialization.jsonObject(with: _jsonData, options: .allowFragments)
    var result: Any? = jsonObject
    var abort = false
    var next = jsonObject as? [String: Any]
    paths.forEach({ (seg) in
        if seg.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) == "" || abort {
            return
        }
        if let _next = next?[seg] {
            result = _next
            next = _next as? [String: Any]
        } else {
            abort = true
        }
    })
    //判断条件保证返回正确结果,保证没有流产,保证jsonObject转换成了Data类型
    guard abort == false,
        let resultJsonObject = result,
        let data = try? JSONSerialization.data(withJSONObject: resultJsonObject, options: []) else {
        return nil
    }
    return data
}

CodableHelper的使用也非常简单,有点面向对象变成的感觉,下面是具体的使用例子。

struct Person: Codable {
    var name: String?
    var age: Int?
    var sex: String?
}


//jsonString中获取数据封装成Model
let p1String = "{"name":"walden","age":30,"sex":"man"}"
let p1 = Person.decodeJSON(from: p1String)

//jsonString中获取数据封装成Array
let personString = "{"haha":[{"name":"walden","age":30,"sex":"man"},{"name":"healer","age":20,"sex":"female"}]}"
let persons = [Person].decodeJSON(from: personString, designatedPath: "haha")

//对象转jsonString
let jsonString = p1?.toJSONString()

//对象转jsonObject
let jsonObject = p1?.toJSONObject()

附:swift.ctolib.com/HandyJSON.h…