Codable 宏让 Swift 序列化如此简单!
大家好!作为 Swift 开发者,我们每天都在与数据打交道,而 JSON 与模型之间的转换无疑是家常便饭。苹果为我们提供了 Codable
协议,它在很多情况下表现出色,但随着业务逻辑变得复杂,我们常常会陷入编写大量样板代码的泥潭:手动定义 CodingKeys
、实现 init(from:)
和 encode(to:)
、处理嵌套结构、应对不同的命名风格、解析各种日期格式…… 这些繁琐的任务不仅耗时,还容易出错。
有没有更优雅、更高效的方式来处理 Swift 中的 Codable 呢?
答案是肯定的!随着 Swift 5.9+ 引入的 Swift Macros,代码生成的可能性被大大扩展。今天,我向大家介绍一款基于 Swift Macros 构建的框架 —— ReerCodable!
ReerCodable (github.com/reers/ReerC…) 旨在通过声明式的注解,彻底简化 Codable
的使用体验,让你告别繁琐的样板代码,专注于业务逻辑本身。
实际应用示例
让我们通过一个实际的例子来看看 ReerCodable 如何简化开发工作。假设我们有一个复杂的 API 响应:
{
"code": 0,
"data": {
"user_info": {
"user_name": "phoenix",
"birth_date": "1990-01-01T00:00:00Z",
"location": {
"city": "北京",
"country": "中国"
},
"height_in_meters": 1.85,
"is_vip": true,
"tags": ["tech", null, "swift"],
"last_login": 1731585275944
}
}
}
使用 ReerCodable,我们可以这样定义模型:
@Codable
struct ApiResponse {
var code: Int
@CodingKey("data.user_info")
var userInfo: UserInfo
}
@Codable
@SnakeCase
struct UserInfo {
var userName: String
@DateCoding(.iso8601)
var birthDate: Date
@CodingKey("location.city")
var city: String
@CodingKey("location.country")
var country: String
@CustomCoding<Double>(
decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
)
var heightInCentimeters: Double
var isVip: Bool
@CompactDecoding
var tags: [String]
@DateCoding(.millisecondsSince1970)
var lastLogin: Date
}
// 使用方式
do {
let response = try ApiResponse.decode(from: jsonString)
print("用户名: (response.userInfo.userName)")
print("出生日期: (response.userInfo.birthDate)")
print("身高(厘米): (response.userInfo.heightInCentimeters)")
} catch {
print("解析失败: (error)")
}
原生 Codable 的那些“痛”
在我们深入了解 ReerCodable 的魅力之前,先来回顾一下使用原生 Codable
时可能遇到的常见痛点:
-
手动
CodingKeys
: 当 JSON key 与属性名不一致时,需要手动编写CodingKeys
枚举,只修改一个, 其他所有属性都要写, 属性少还好,一旦多了,简直是噩梦。 - 嵌套 Key: 处理深层嵌套的 JSON 数据,需要定义多个中间结构体或手动编写解码逻辑。
- 命名风格转换: 后端返回 snake_case 或 kebab-case,而 Swift 推荐 camelCase,需要手动映射。
-
复杂的解码逻辑: 如需自定义解码(类型转换、数据修复等),就得实现
init(from:)
。 -
默认值处理: 非 Optional 属性在 JSON 中缺失时,即使有默认值也会抛出
keyNotFound
异常。Optional 枚举缺失也会导致解码失败。 -
忽略属性: 某些属性不需要参与编解码,需要手动在
CodingKeys
或实现中处理。 -
日期格式多样: 时间戳、ISO8601、自定义格式…… 需要为
JSONDecoder
配置不同的dateDecodingStrategy
或手动处理。 -
集合中的
null
: 数组或字典中包含null
值时,若对应类型非 Optional,会导致解码失败。 -
继承: 父类的属性无法在子类的
Codable
实现中自动处理。 -
枚举处理: 关联值枚举或需要匹配多种原始值的枚举,原生
Codable
支持有限。
社区现状
Swift 社区为了解决 JSON 序列化的问题,涌现出了许多优秀的第三方框架。了解它们的设计哲学和优缺点,能帮助我们更好地理解为什么基于 Swift Macros 的方案是当下更优的选择。
1. 基于自定义协议的框架
ObjectMapper
ObjectMapper 是最早的一批 Swift JSON 解析库之一,它基于自定义的 Mappable
协议:
class User: Mappable {
var name: String?
var age: Int?
required init?(map: Map) {}
func mapping(map: Map) {
name <- map["user_name"]
age <- map["user_age"]
}
}
特点:
- 不依赖 Swift 的原生 Codable
- 不依赖反射机制
- 自定义操作符
<-
使映射代码简洁 - 需要手动编写映射关系
- 支持嵌套映射和自定义转换
ObjectMapper 的优点是代码相对简洁,且不依赖 Swift 的内部实现细节,但缺点是需要手动编写映射代码,并且与 Swift 的原生序列化机制不兼容。
2. 基于运行时反射的框架
HandyJSON 和 KakaJSON
这两个框架采用了相似的实现原理,都是通过运行时反射获取类型信息:
struct User: HandyJSON {
var name: String?
var age: Int?
}
// 使用
let user = User.deserialize(from: jsonString)
特点:
- 通过底层运行时反射获取类型元数据
- 直接操作内存进行属性赋值
- 几乎无需编写额外代码
- 性能较高
这类框架的主要问题是强依赖 Swift 的内部实现细节和元数据结构(Metadata),随着 Swift 版本升级,容易出现不兼容问题或崩溃。它们实现了"零代码"的理想,但牺牲了稳定性和安全性。
3. 基于属性包装器(Property Wrapper)的框架
ExCodable 和 BetterCodable
这些框架利用 Swift 5.1 引入的属性包装器特性,为 Codable 提供扩展:
struct User: Codable {
@CustomKey("user_name")
var name: String
@DefaultValue(33)
var age: Int
}
特点:
- 基于 Swift 的原生 Codable
- 使用属性包装器简化常见编解码任务
- 无需手动编写 CodingKeys 和 Codable 实现
- 类型安全,编译时检查
属性包装器方案相比前两类有明显的优势:它既保持了与 Swift 原生 Codable 的兼容性,又简化了代码编写。但 PropertyWrapper 能力有限, 有些复杂的封装设计做不到.
4. 基于宏(Macro)的框架
CodableWrapper、CodableWrappers 和 MetaCodable,以及本文的 ReerCodable
这些框架利用 Swift 5.9 引入的宏特性,在编译时生成 Codable 的实现代码:
@Codable
struct User {
@CodingKey("user_name")
var name: String
var age: Int = 33
}
特点:
- 基于 Swift 的原生 Codable
- 声明式语法,直观易懂
- 高度灵活,支持复杂的编解码逻辑
- 可以在类型级别应用宏
宏方案结合了所有前述方案的优点,同时避免了它们的缺点:它基于原生 Codable,保持类型安全;它支持声明式语法,代码简洁;它在编译时生成代码,没有运行时性能损失;它能够处理复杂场景,适应性强。
为什么 Macro 是最优雅的解决方案?
在所有这些框架中,基于宏的方案(如 ReerCodable)提供了最优雅的解决方案,原因如下:
- 与原生 Codable 无缝集成:生成的代码与手写的 Codable 实现完全相同,可以与其他使用 Codable 的 API 完美配合。对于现代三方框架如 Alamofire, GRDB 等都与 Codable 互相兼容..
- 声明式语法:通过注解方式声明序列化需求,代码简洁直观,意图明确。
- 类型安全:所有操作都在编译时进行类型检查,避免运行时错误。
- 高度灵活:可以处理各种复杂场景,如嵌套结构、自定义转换、条件编解码等。
- 维护性好:宏生成的代码是可预测的,而且不依赖于 Swift 的内部实现细节,随着 Swift 版本更新不会出现兼容性问题。
- 可调试性强:可以查看宏展开后的代码,便于理解和调试。
- 可扩展性:可以组合使用不同的宏,构建复杂的编解码逻辑。
ReerCodable 登场:化繁为简的魔法
ReerCodable 利用 Swift Macros 的强大能力,让你只需在类型或属性前添加简单的注解,就能自动生成高效、健壮的 Codable
实现。核心就是 @Codable
宏,它会与其他 ReerCodable 提供的宏协同工作,生成最终的编解码逻辑。框架接入支持 Cocoapods, SwiftPackageManager。
代码实现上参考了优秀的 winddpan/CodableWrapper、GottaGetSwifty/CodableWrappers 和 MetaCodable,相对它们 ReerCodable 有更丰富的 feature 或更简洁的使用。
让我们来看看 ReerCodable 如何优雅地解决上述痛点:
1. 自定义 CodingKey
通过 @CodingKey
可以为属性指定自定义 key,无需手动编写 CodingKeys
枚举:
ReerCodable | Codable |
---|---|
|
|
2. 嵌套 CodingKey
支持通过点语法表示嵌套的 key path:
@Codable
struct User {
@CodingKey("other_info.weight")
var weight: Double
@CodingKey("location.city")
var city: String
}
3. 多键解码
可以指定多个 key 用于解码,系统会按顺序尝试解码直到成功:
@Codable
struct User {
@CodingKey("name", "username", "nick_name")
var name: String
}
4. 命名转换
支持多种命名风格转换,可以应用在类型或单个属性上:
@Codable
@SnakeCase
struct Person {
var firstName: String // 从 "first_name" 解码, 或编码为 "first_name"
@KebabCase
var lastName: String // 从 "last-name" 解码, 或编码为 "last-name"
}
5. 自定义编解码容器
使用 @CodingContainer
自定义编解码时的容器路径, 通常用于JSON嵌套较多, 但 model 声明 想直接 match 子层级结构:
ReerCodable | JSON |
---|---|
|
|
6. 编码专用 key
可以为编码过程指定不同的键名, 由于 @CodingKey
可能有多个参数, 再加上可以使用 @SnakeCase
, KebabCase
等, 解码可能使用多个 key, 那编码时会采用第一个 key, 也可以通过 @EncodingKey
来指定 key
@Codable
struct User {
@CodingKey("user_name") // 解码使用 "user_name", "name"
@EncodingKey("name") // 编码使用 "name"
var name: String
}
7. 默认值支持
解码失败时可以使用默认值, 原生 Codable
针对非 Optional
属性, 会在没有解析到正确值是抛出异常, 即使已经设置了初始值, 或者即使是 Optional
类型的枚举
@Codable
struct User {
var age: Int = 33
var name: String = "phoenix"
// 若 JSON 中不包含 gender, 原生 Codable 会抛出异常, ReerCodable 不会, 会设置其为 nil
var gender: Gender?
}
enum Gender {
case male, female
}
8. 忽略属性
使用 @CodingIgnored
在编解码过程中忽略特定属性. 在解码过程中对于非 Optional
属性要有一个默认值才能满足 Swift 初始化的要求, ReerCodable
对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.
@Codable
struct User {
var name: String
@CodingIgnored
var ignore: Set<String>
}
9. Base64 编解码
自动处理 base64 字符串与 Data
, [UInt8]
类型的转换:
@Codable
struct User {
@Base64Coding
var avatar: Data
@Base64Coding
var voice: [UInt8]
}
10. 集合类型解码优化
使用 @CompactDecoding
在解码数组时自动过滤 null 值, 与 compactMap
是相同的意思:
@Codable
struct User {
@CompactDecoding
var tags: [String] // ["a", null, "b"] 将被解码为 ["a", "b"]
}
同时, Dictionary
和 Set
也支持使用 @CompactDecoding
来优化
11. 日期编解码
支持多种日期格式的编解码:
ReerCodable | JSON |
---|---|
|
|
12. 自定义编解码逻辑
通过 @CustomCoding
实现自定义的编解码逻辑. 自定义编解码有两种方式:
- 通过闭包, 以
decoder: Decoder
,encoder: Encoder
为参数来实现自定义逻辑:
@Codable
struct User {
@CustomCoding<Double>(
decode: { return try $0.value(forKeys: "height_in_meters") * 100.0 },
encode: { try $0.set($1 / 100.0, forKey: "height_in_meters") }
)
var heightInCentimeters: Double
}
- 通过一个实现
CodingCustomizable
协议的自定义类型来实现自定义逻辑:
// 1st 2nd 3rd 4th 5th -> 1 2 3 4 5
struct RankTransformer: CodingCustomizable {
typealias Value = UInt
static func decode(by decoder: any Decoder, keys: [String]) throws -> UInt {
var temp: String = try decoder.value(forKeys: keys)
temp.removeLast(2)
return UInt(temp) ?? 0
}
static func encode(by encoder: Encoder, key: String, value: Value) throws {
try encoder.set(value, forKey: key)
}
}
@Codable
struct HundredMeterRace {
@CustomCoding(RankTransformer.self)
var rank: UInt
}
自定义实现过程中, 框架提供的方法也可以使编解码更加方便:
public extension Decoder {
func value<Value: Decodable>(forKeys keys: String...) throws -> Value {
let container = try container(keyedBy: AnyCodingKey.self)
return try container.decode(type: Value.self, keys: keys)
}
}
public extension Encoder {
func set<Value: Encodable>(_ value: Value, forKey key: String, treatDotAsNested: Bool = true) throws {
var container = container(keyedBy: AnyCodingKey.self)
try container.encode(value: value, key: key, treatDotAsNested: treatDotAsNested)
}
}
13. 继承支持
使用 @InheritedCodable
更好地支持子类的编解码. 原生 Codable
无法解析子类属性, 即使 JSON 中存在该值, 需要手动实现 init(from decoder: Decoder) throws
@Codable
class Animal {
var name: String
}
@InheritedCodable
class Cat: Animal {
var color: String
}
14. 枚举支持
为枚举提供丰富的编解码能力:
- 对基本枚举类型, 以及 RawValue 枚举支持
@Codable
struct User {
let gender: Gender
let rawInt: RawInt
let rawDouble: RawDouble
let rawDouble2: RawDouble2
let rawString: RawString
}
@Codable
enum Gender {
case male, female
}
@Codable
enum RawInt: Int {
case one = 1, two, three, other = 100
}
@Codable
enum RawDouble: Double {
case one, two, three, other = 100.0
}
@Codable
enum RawDouble2: Double {
case one = 1.1, two = 2.2, three = 3.3, other = 4.4
}
@Codable
enum RawString: String {
case one, two, three, other = "helloworld"
}
- 支持使用
CodingCase(match: ....)
来匹配多个值或 range
@Codable
enum Phone: Codable {
@CodingCase(match: .bool(true), .int(10), .string("iphone"), .intRange(22...30))
case iPhone
@CodingCase(match: .int(12), .string("MI"), .string("xiaomi"), .doubleRange(50...60))
case xiaomi
@CodingCase(match: .bool(false), .string("oppo"), .stringRange("o"..."q"))
case oppo
}
- 对于有关联值的枚举, 支持通用
CaseValue
来匹配关联值, 使用.label()
来声明有标签的关联值的匹配逻辑, 使用.index()
来声明没有标签的的关联值的匹配逻辑.ReerCodable
支持两种JSON 格式的枚举匹配- 第一种是也是原生
Codable
支持的, 即枚举值和其关联值是父子级的结构:
@Codable enum Video: Codable { /// { /// "YOUTUBE": { /// "id": "ujOc3a7Hav0", /// "_1": 44.5 /// } /// } @CodingCase(match: .string("youtube"), .string("YOUTUBE")) case youTube /// { /// "vimeo": { /// "ID": "234961067", /// "minutes": 999999 /// } /// } @CodingCase( match: .string("vimeo"), values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")] ) case vimeo(id: String, duration: TimeInterval = 33, Int) /// { /// "tiktok": { /// "url": "https://example.com/video.mp4", /// "tag": "Art" /// } /// } @CodingCase( match: .string("tiktok"), values: [.label("url", keys: "url")] ) case tiktok(url: URL, tag: String?) }
- 第二种是枚举值和其关联值同级或自定义匹配的结构, 使用
.pathValue()
进行自定义路径值的匹配
@Codable enum Video1: Codable { /// { /// "type": { /// "middle": "youtube" /// } /// } @CodingCase(match: .pathValue("type.middle.youtube")) case youTube /// { /// "type": "vimeo", /// "ID": "234961067", /// "minutes": 999999 /// } @CodingCase( match: .pathValue("type.vimeo"), values: [.label("id", keys: "ID", "Id"), .index(2, keys: "minutes")] ) case vimeo(id: String, duration: TimeInterval = 33, Int) /// { /// "type": "tiktok", /// "media": "https://example.com/video.mp4", /// "tag": "Art" /// } @CodingCase( match: .pathValue("type.tiktok"), values: [.label("url", keys: "media")] ) case tiktok(url: URL, tag: String?) }
- 第一种是也是原生
15. 生命周期回调
支持编解码的生命周期回调:
@Codable
class User {
var age: Int
func didDecode(from decoder: any Decoder) throws {
if age < 0 {
throw ReerCodableError(text: "Invalid age")
}
}
func willEncode(to encoder: any Encoder) throws {
// 在编码前进行处理
}
}
@Codable
struct Child: Equatable {
var name: String
mutating func didDecode(from decoder: any Decoder) throws {
name = "reer"
}
func willEncode(to encoder: any Encoder) throws {
print(name)
}
}
16. JSON 扩展支持
提供便捷的 JSON 字符串和字典转换方法:
let jsonString = "{\"name\": \"Tom\"}"
let user = try User.decode(from: jsonString)
let dict: [String: Any] = ["name": "Tom"]
let user2 = try User.decode(from: dict)
17. 基本类型转换
支持基本数据类型之间的自动转换:
@Codable
struct User {
@CodingKey("is_vip")
var isVIP: Bool // "1" 或 1 都可以被解码为 true
@CodingKey("score")
var score: Double // "100" 或 100 都可以被解码为 100.0
}
18. AnyCodable 支持
通过 AnyCodable
实现对 Any
类型的编解码:
@Codable
struct Response {
var data: AnyCodable // 可以存储任意类型的数据
var metadata: [String: AnyCodable] // 相当于[String: Any]类型
}
19. 生成默认实例
@Codable
@DefaultInstance
struct ImageModel {
var url: URL
}
@Codable
@DefaultInstance
struct User5 {
let name: String
var age: Int = 22
var uInt: UInt = 3
var data: Data
var date: Date
var decimal: Decimal = 8
var uuid: UUID
var avatar: ImageModel
var optional: String? = "123"
var optional2: String?
}
会生成以下实例
static let `default` = User5(
name: "",
age: 22,
uInt: 3,
data: Data(),
date: Date(),
decimal: 8,
uuid: UUID(),
avatar: ImageModel.default,
optional: "123",
optional2: nil
)
⚠️注意: 泛型类型的属性不支持使用 @DefaultInstance
@Codable
struct NetResponse<Element: Codable> {
let data: Element?
let msg: String
private(set) var code: Int = 0
}
20. 生成 copy 方法
使用 Copyable
为模型生成 copy
方法
@Codable
@Copyable
public struct Model6 {
var name: String
let id: Int
var desc: String?
}
@Codable
@Copyable
class Model7<Element: Codable> {
var name: String
let id: Int
var desc: String?
var data: Element?
}
生成如下 copy
方法, 可以看到, 除了默认 copy, 还可以对部分属性进行更新
public func copy(
name: String? = nil,
id: Int? = nil,
desc: String? = nil
) -> Model6 {
return .init(
name: name ?? self.name,
id: id ?? self.id,
desc: desc ?? self.desc
)
}
func copy(
name: String? = nil,
id: Int? = nil,
desc: String? = nil,
data: Element? = nil
) -> Model7 {
return .init(
name: name ?? self.name,
id: id ?? self.id,
desc: desc ?? self.desc,
data: data ?? self.data
)
}
以上示例展示了 ReerCodable 的主要特性,这些特性可以帮助开发者大大简化编解码过程,提高代码的可读性和可维护性。
ReerCodable 通过一系列精心设计的 Swift Macros,极大地简化了 Codable
的使用,显著减少了样板代码,提高了开发效率和代码可读性。它不仅涵盖了原生 Codable
的大部分场景,还提供了更强大、更灵活的功能,如多 key 解码、命名转换、自定义容器、健壮的默认值处理、强大的枚举支持以及便捷的辅助工具等。
如果你还在为 Codable
的繁琐实现而烦恼,不妨试试 ReerCodable,相信它会给你带来惊喜!
GitHub 地址: github.com/reers/ReerC…
欢迎大家试用、Star、提 Issue 或 PR!让我们一起用更现代、更优雅的方式来编写 Swift 代码!
文章主要由 AI 生成, 具体以 github readme 为准