0x00 Codable 介绍
Codable是从Swift 4开始加入到Swift的标准库组件,其提供了一种非常简单的方式支持模型和数据之间的转换。
当时官方发布的时候就说过,Codable的设计目标有三个:通用性、类型安全性以及减少编码过程的模板代码。
0x01 为什么要去学习 Codable
- 解决序列化与反序列化
- 替代现有基于 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 的方式只用来给服务器上传即可
多态的时候尽量使用协议来实现映射
引用: