Codable 学习

0x00 Codable 介绍

Codable是从Swift 4开始加入到Swift的标准库组件,其提供了一种非常简单的方式支持模型和数据之间的转换。

当时官方发布的时候就说过,Codable的设计目标有三个:通用性、类型安全性以及减少编码过程的模板代码。

0x01 为什么要去学习 Codable

  1. 解决序列化与反序列化
  2. 替代现有基于 ABI 不稳定的方案
    • SwiftJSON
    • HandyJSON
    • KakaJSON

Swift发布4.0版本之前,官方未提供推荐的 JSON 处理方案,因此我们项目使用了HandyJSON 这套方案

但是, HandyJSON 的实现强依赖于Swift底层内存布局机制,这个机制是非公开、不被承诺、且实践证明一直在随着Swift 版本变动的,HandyJSON 需要跟进 Swift 的每次版本更新,更大的风险是,用户升级 iOS 版本可能会影响这个依赖,导致应用逻辑异常

0x02 怎么样使用 Codable

代码如下:

// 定义一个模型, 支持 Codable 协议
struct Person: Codable {
    let name: String
    let age: Int
    var test: Int?
}

// 解码 JSON 数据
let json = #" {"name":"Tom", "age": 2} "#
let person = try JSONDecoder().decode(Person.self, from: json.data(using: .utf8)!)

print(person)

print("\n")

// 编码导出为 JSON 数据
let data0 = try? JSONEncoder().encode(person)
let dataObject = try? JSONSerialization.jsonObject(with: data0!)
print(dataObject ?? "nil")

print("\n")

let data1 = try? JSONSerialization.data(withJSONObject: ["name": person.name, "age": person.age])
print(String(data: data1!, encoding: .utf8)!)

输出结果如下:

Person(name: "Tom", age: 2, test: nil)


{
    age = 2;
    name = Tom;
}


{"name":"Tom","age":2}

0x03 分析与讨论

上面的使用方式是一个最简单的最直接的 Codable 引用, 其实 Codable 还有很多使用方式以及问题

自定义 Key

使用以前的序列化工具都需要考虑的是自定义 Key, 比如服务器给的是 { "first_name": "Tom" }, 但是 APP 习惯是驼峰命名, 这时候就需要自定义 Key 了

当然只是驼峰命名的话, 系统有封装 decorder.keyDecodingStrategy = .convertFromSnakeCase 即可实现, 后面的嵌套例子会用到, 其他的自定义 Key 就要自己实现了

struct Person: Codable {
    let name: String
    let age: Int
    let firstName: String
    
    enum CodingKeys: String, CodingKey {
        case name, age
        case firstName = "first_name"
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        age = try values.decode(Int.self, forKey: .age)
        firstName = try values.decode(String.self, forKey: .firstName)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(age, forKey: .age)
        try container.encode(firstName, forKey: .firstName)
    }
}

let data = #"{"name": "Tom", "age": 10, "first_name": "James"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Encoder 的三个接口

上面的 func encode(to encoder: Encoder) 里面使用了其中一个, 以下是三个接口

如果模型想要 key -> value, 就使用
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey 接口作为容器, 这样 encode 的结果就是一个可以转换成字符串的 json data

如果模型想要忽略 key 值, 以 value 组成数组的方式 encode , 就使用 func unkeyedContainer() -> UnkeyedEncodingContainer 接口作为容器, 这样的 encode 就是一个当前层级为数组的 data

如果模型只想要在 encode 的时候保留其中一个值或者只有一个值的时候, 使用func singleValueContainer() -> SingleValueEncodingContainer 接口做为容器, 这样 encode 的就是一个单一结果

// 还是上面的代码

// 控制台输出 encode 结果, {"name": "Tom", "age": 10, "first_name": "James"}
func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(age, forKey: .age)
    try container.encode(firstName, forKey: .firstName)
}

// 控制台输出 encode 结果, ["Tom",10,"James"]
func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(name)
    try container.encode(age)
    try container.encode(firstName)
}

// 控制台输出 encode 结果, "Tom"
func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(name)
}

Embed 嵌套类型

最常见的数据结构, 一个模型内有多个模型或者模型数组存在, 只要实现了 Codable 协议, 系统会自动为我们完成嵌套内容, 每一层只需要关心自己的 Codable 实现即可

struct Person: Codable, CustomStringConvertible {
    let name: String
    let age: Int
    
    var description: String {
        "name: \(name) age: \(age)"
    }
}

struct Family: Codable, CustomStringConvertible {
    let familyName: String
    let persons: [Person]
    
    var description: String {
        "familyName: \(familyName)\npersons: \(persons)"
    }
}


let data = """
{
    "family_name": "101",
    "persons":[
          {
             "name": "小明",
             "age": 1
          },
          {
             "name": "小红",
             "age": 1
          }
    ]
}
""".data(using: .utf8)!

do {
    let decorder = JSONDecoder()
    decorder.keyDecodingStrategy = .convertFromSnakeCase
    let family = try decorder.decode(Family.self, from: data)
    print(family)
} catch {
    print(error)
}

输出结果为:

familyName: 101
persons: [name: 小明 age: 1, name: 小红 age: 1]

支持日期格式

只要满足 formatter 格式的都会自动转换

struct Person: Codable {
    let birthday: Date
}

//let data = """
//{
//    "birthday": "2022-10-20T14:15:00-0000"
//}
//""".data(using: .utf8)!

//let data = """
//{
//    "birthday": 1666182937
//}
//""".data(using: .utf8)!

let data = """
{
    "birthday": "2022-10-19 20:35:37.000000"
}
""".data(using: .utf8)!

do {
    // create a date formatter
    let dateFormatter = DateFormatter()
    // set time zone
    dateFormatter.timeZone = TimeZone(identifier: "Asia/Shanghai") ?? .current
    // setup formate string for the date formatter
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSSSS"
    
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(dateFormatter)

    let person = try decoder.decode(Person.self, from: data)
    print(person)
    
    print(dateFormatter.string(from: person.birthday))
} catch {
    print(error)
}

网络请求中序列化问题

网络请求都会有能用枚举表示的, Swift 的枚举和 OC 的不一样, 初始化不了就是 nil, 所以下面的代码会报错, dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "stageId", intValue: nil)], debugDescription: "Cannot initialize StageIDType from invalid Int value 99999", underlyingError: nil))

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }

    enum StageIDType: Int, Codable {
        case preSchool = 9999
        case primary = 10001
        case junior = 10002
        case senior = 10003
    }

    let name: String
    var gender: Gender = .male
    var stageId: StageIDType = .junior

    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

// 不给予默认值, 会报数据错误
// 给予默认值, 依旧会数据错误
let data = #"{"name": "123", "gender": 1, "stageId": 99999}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
} catch {
    print(error)
}

Codable 中意外值

上面的错误, 可以对枚举做二次封装来匹配, 但是这样的封装使用起来会很费事, 需要 switch 等方式取值, 变通一下这种意外方式, 使用结构体+静态属性

struct Person: Codable, CustomStringConvertible {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    var description: String {
        "name: \(name)\ngender: \(gender)\nstageId: \(stageId)"
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1004}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    let data = try JSONEncoder().encode(person)
    print(String(data: data, encoding: .utf8)!)
} catch {
    print(error)
}

输出如下:

name: 123
gender: female
stageId: StageIDType(rawValue: 1004)
{"name":"123","stageId":1004,"gender":1}

Codable 中默认值

在开发的时候, 默认值也很重要, 这里可以考虑用属性包装器, 做一下封装来用, 只是一个思想, 关于 Codable 封装好的三方库也是有一些的, 至于用得上用不上就看开发人员自己选择吧, 我们项目当中暂时还没有这种需求

struct Person: Codable {
    enum Gender: Int, Codable {
        case male
        case female
    }
    
    struct StageIDType: RawRepresentable, Codable {
        typealias RawValue = Int
        
        let rawValue: RawValue
        
        static let preSchool: StageIDType = .init(rawValue: 9999)
        static let primary: StageIDType = .init(rawValue: 1001)
        static let junior: StageIDType = .init(rawValue: 1002)
        static let senior: StageIDType = .init(rawValue: 1003)
    }
    
    let name: String
    let gender: Gender
    let stageId: StageIDType
    
    @Default<Bool>(true)
    var canPlayBall: Bool
}

protocol DefaultValuable {
    associatedtype Value: Codable
    static var defaultValue: Value { get }
}

extension Bool: DefaultValuable {
    static let defaultValue = true
}

extension Int: DefaultValuable {
    static var defaultValue: Int {
        100
    }
}

@propertyWrapper
struct Default<T: DefaultValuable>: Codable {
    var wrappedValue: T.Value
    
    init(_ wrappedValue: T.Value) {
        self.wrappedValue = wrappedValue
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

extension KeyedDecodingContainer {
    func decode<T>(
        _ type: Default<T>.Type,
        forKey key: Key
    ) throws -> Default<T> where T: DefaultValuable {
        return try decodeIfPresent(type, forKey: key) ?? Default(T.defaultValue)
    }
}

let data = #"{"name": "123", "gender": 1, "stageId": 1001, "canPlayBall": "12"}"#.data(using: .utf8)
do {
    let person = try JSONDecoder().decode(Person.self, from: data!)
    print(person)
    
    let person1 = Person(name: "dsad", gender: .male, stageId: .senior)
    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let data = try encoder.encode(person1)
    let json = String(data: data, encoding: .utf8)!
    print(json)
} catch {
    print(error)
}

输出结果如下:

Person(name: "123", gender: __lldb_expr_119.Person.Gender.female, stageId: __lldb_expr_119.Person.StageIDType(rawValue: 1001), _canPlayBall: __lldb_expr_119.Default<Swift.Bool>(wrappedValue: true))
{
  "gender" : 0,
  "stageId" : 1003,
  "name" : "dsad",
  "canPlayBall" : true
}

0x04 总结

有兴趣的可以看看下面引用中的源码链接, 里面的代码很好, 其中 Codable.swift 实现了接口协议以及基础数据类型的 encoder decoder 的默认实现, JSONEncoder.swift 实现了具体功能

通过反射和内存操作的那些库, 比如 HandyJSON 优势是可以设置默认值, 模型定义的 key 在 json 中不存在不会报错, 会忽略, 而 Codable 需要可选值才能标识忽略, Codable 不自定义 decode 都加问号(基础数据类型), 或者自定义 decode 并添加默认值

利用系统提供的便利性, 尽量在 Codable 处使用嵌套并拥有基础数据类型, 这样编译器会在编译的时候生成模版代码

使用了 Codable 就尽量不要使用字典, Codable 意味着具象类型要以对象为单位, toJSONObject 的方式只用来给服务器上传即可

多态的时候尽量使用协议来实现映射


引用:

Codable 使用小记

用 Codable 协议实现快速 JSON 解析

Codable源码剖析

源码解读——Codable

Property Wrapper 为 Codable 解码设定默认值

如何优雅的使用Swift Codable协议

Codable保姆级攻略

Codable.swift 源码

JSONEncoder.swift 源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容