Swift - Codable

970 阅读5分钟

这是我参与更文挑战的第14天,活动详情查看: 更文挑战

Codable是什么?

/// A type that can convert itself into and out of an external representation.
///
/// `Codable` is a type alias for the `Encodable` and `Decodable` protocols.
/// When you use `Codable` as a type or a generic constraint, it matches
/// any type that conforms to both protocols.
typealias Codable = Decodable & Encodable

protocol Encodable {
    func encode(to encoder: Encoder) throws
}

protocol Decodable {
    init(from decoder: Decoder) throws
}

可以看到它实际上是 Decodable 和 Encodeable 的复合接口,用作数据的解析和编码;Swift4.0标准库中正式引入了Codable接口;

先简单看一个Model to JSON 的例子,感受下官方Codable的逻辑

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let banner = GroceryProduct(name: "香蕉", points: 250, description: "海南产的")

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

/// 对banner 实例进行JSON编码
let data = try encoder.encode(banner)
print(String(data: data, encoding: .utf8)!)

/* 打印:
 {
   "name" : "Pear",
   "points" : 250,
   "description" : "A ripe pear."
 }
*/


/// 对 JSON String 转Model 实例

let json = """
{
    "name": "榴莲",
    "points": 600,
    "description": "一般人很难接受的水果"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)

print(product.name) // 打印 "榴莲"

过滤编码属性

在json的序列化中可以过滤掉部分的字段,只需要声明一个 CodingKeys 枚举属性

struct GroceryProduct: Codable {
    var name: String
    var points: Int = 0
    var description: String?
    
    enum CodingKeys: String, CodingKey {
        case name
        case description
    }
}

输出结果

{"name":"香蕉","description":"海南产的"}

键值修改

有时候服务下发的字段名不是我们想要的,比如接口表示性别使用的是gender,而有些接口使用确实sex,为了适应不用的接口,也可以通过CodingKeys 改变编码属性的名称

struct GroceryProduct: Codable {
    var name: String
    var points: Int = 0
    var description: String?
    
    enum CodingKeys: String, CodingKey {
        case name
        case description = "desc"
    }
}

输出结果:

{"name":"香蕉","desc":"海南产的"}

键值策略自定义

在日常使用,我们总会碰到不同的命名规则,例如下划线命名(person_name)和驼峰命名(personName)以及首字母大写的帕斯卡命名(PersonName),假设你和后端并没有统一命名风格,那么自定义键值或许还能弥补一下。

第一种方式是直接在CodingKeys直接修改:

struct Person: Codable {
    var firstName: String
    var secondName: String
    
    enum CodingKeys: String, CodingKey {
        case firstName = "first_name"
        case secondName = "second_name"
    }
}

第二种方式是使用更通用的键值转换策略来解决JSON解码和编码的键策略问题。

 /// The strategy to use for automatically changing the value of keys before encoding.
    public enum KeyEncodingStrategy {
    ...
    /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
    case custom(([CodingKey]) -> CodingKey)
    }
  /// The strategy to use for automatically changing the value of keys before decoding.
    public enum KeyDecodingStrategy {
    ...
    /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
    case custom(([CodingKey]) -> CodingKey)
    }
// 对person实例进行Json编码
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = 自定义

let data = try! encoder.encode(person)
let personEncodedString = String(data: data, encoding: .utf8)!
print(personEncodedString)

结构自定义


struct Person: Codable {
    var gender: String
    var age: Int
    var name: String

    var firstName: String
    var secondName: String

    enum CodingKeys: String, CodingKey {
        case gender
        case age
        case name
    }

    enum NameKeys: String, CodingKey {
        case firstName
        case secondName
    }
}

extension Person {
    //解析
    init(from decoder: Decoder) throws {
        let vals = try decoder.container(keyedBy: CodingKeys.self)
        gender = try vals.decode(String.self, forKey: CodingKeys.gender)
        age = try vals.decode(Int.self, forKey: CodingKeys.age)
				
      	// nestedContainer 解析name属性
        let name = try vals.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        firstName = try name.decode(String.self, forKey: .firstName)
        secondName = try name.decode(String.self, forKey: .secondName)
    }

    //编码
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(gender, forKey: .gender)
        try container.encode(age, forKey: .age)
					
        //nestedContaine 解析name节点
        var name = container.nestedContainer(keyedBy: NameKeys.self, forKey: .name)
        try name.encode(firstName, forKey: .firstName)
        try name.encode(secondName, forKey: .secondName)
    }
}

三方JSON解析库

在OC中,我们有很多优秀的第三方库帮助我们实现,比如MJExtension、JSONModel等,这些库基本都是利用runtime实现读取属性名并利用kvc重新赋值属性。

在Swift中,由于runtime的局限,比较出名的有SwiftyJSON、ObjectMapper、HandyJSON等。

其中

1、SwiftyJSON本质上仍然是根据JSON结构去取值,使用起来顺手、清晰;

2、ObjectMapper实现了JSON直接转Model的功能,不过使用起来,代码量会多一点,因为我们必须遵循Mappable协议,制定json内的每一个key和model属性的对应关系。

3、 HandyJSON另辟蹊径,采用Swift反射+内存赋值的方式来构造Model实例,保持原汁原味的Swift类定义。

HandyJSON

// 假设这是服务端返回的统一定义的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)
}

  1. HandyJSON支持 JSON直接转Model,定义class时,有两点注意:
  • 必须遵循HandyJSON协议
  • 需要实现空的initializer (当然Struct结构体 可以不需要init())
  1. HandyJSON还支持Struct,使用方式与Class基本一致

  2. HandyJSON支持枚举,只需要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 = Animal.deserialize(from: jsonString) {
        print(animal.type?.rawValue)
    }
    
  3. 此外,HandyJSON还支持一些非基础类型、复杂类型,包括嵌套结构,如可选、隐式解包可选、集合等

  4. 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\"}}}"
    
    ///其中,直接通过 designatedPath 定位到我们需要的节点处。
    if let cat = JSONDeserializer<Cat>.deserializeFrom(json: jsonString, designatedPath: "data.cat") {
        print(cat.name)
    }
    
  5. HandyJSON支持有继承关系的Model类,就是说及时某个类没有实现HandyJSON协议,只要父类有实现,依然可以转化model。

  6. 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字符串
    

总结

对于简单的数据解析,直接使用Codable 就足以

如果是大型项目或有大量复杂JSON对象,以及有自定义相关需求比较多,那建议使用 HandyJSON

参考

www.jianshu.com/p/661b1539d…

www.devler.cn/blog/1

developer.apple.com/documentati…

www.jianshu.com/p/661b1539d…