首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Codable 宏让 Swift 序列化如此简单!

  • 25-04-23 02:01
  • 3088
  • 5317
juejin.cn

大家好!作为 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 响应:

json
代码解读
复制代码
{ "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,我们可以这样定义模型:

swift
代码解读
复制代码
@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 时可能遇到的常见痛点:

  1. 手动 CodingKeys: 当 JSON key 与属性名不一致时,需要手动编写 CodingKeys 枚举,只修改一个, 其他所有属性都要写, 属性少还好,一旦多了,简直是噩梦。
  2. 嵌套 Key: 处理深层嵌套的 JSON 数据,需要定义多个中间结构体或手动编写解码逻辑。
  3. 命名风格转换: 后端返回 snake_case 或 kebab-case,而 Swift 推荐 camelCase,需要手动映射。
  4. 复杂的解码逻辑: 如需自定义解码(类型转换、数据修复等),就得实现 init(from:)。
  5. 默认值处理: 非 Optional 属性在 JSON 中缺失时,即使有默认值也会抛出 keyNotFound 异常。Optional 枚举缺失也会导致解码失败。
  6. 忽略属性: 某些属性不需要参与编解码,需要手动在 CodingKeys 或实现中处理。
  7. 日期格式多样: 时间戳、ISO8601、自定义格式…… 需要为 JSONDecoder 配置不同的 dateDecodingStrategy 或手动处理。
  8. 集合中的 null: 数组或字典中包含 null 值时,若对应类型非 Optional,会导致解码失败。
  9. 继承: 父类的属性无法在子类的 Codable 实现中自动处理。
  10. 枚举处理: 关联值枚举或需要匹配多种原始值的枚举,原生 Codable 支持有限。

社区现状

Swift 社区为了解决 JSON 序列化的问题,涌现出了许多优秀的第三方框架。了解它们的设计哲学和优缺点,能帮助我们更好地理解为什么基于 Swift Macros 的方案是当下更优的选择。

1. 基于自定义协议的框架

ObjectMapper

ObjectMapper 是最早的一批 Swift JSON 解析库之一,它基于自定义的 Mappable 协议:

swift
代码解读
复制代码
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

这两个框架采用了相似的实现原理,都是通过运行时反射获取类型信息:

swift
代码解读
复制代码
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 提供扩展:

swift
代码解读
复制代码
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 的实现代码:

swift
代码解读
复制代码
@Codable struct User { @CodingKey("user_name") var name: String var age: Int = 33 }

特点:

  • 基于 Swift 的原生 Codable
  • 声明式语法,直观易懂
  • 高度灵活,支持复杂的编解码逻辑
  • 可以在类型级别应用宏

宏方案结合了所有前述方案的优点,同时避免了它们的缺点:它基于原生 Codable,保持类型安全;它支持声明式语法,代码简洁;它在编译时生成代码,没有运行时性能损失;它能够处理复杂场景,适应性强。

为什么 Macro 是最优雅的解决方案?

在所有这些框架中,基于宏的方案(如 ReerCodable)提供了最优雅的解决方案,原因如下:

  1. 与原生 Codable 无缝集成:生成的代码与手写的 Codable 实现完全相同,可以与其他使用 Codable 的 API 完美配合。对于现代三方框架如 Alamofire, GRDB 等都与 Codable 互相兼容..
  2. 声明式语法:通过注解方式声明序列化需求,代码简洁直观,意图明确。
  3. 类型安全:所有操作都在编译时进行类型检查,避免运行时错误。
  4. 高度灵活:可以处理各种复杂场景,如嵌套结构、自定义转换、条件编解码等。
  5. 维护性好:宏生成的代码是可预测的,而且不依赖于 Swift 的内部实现细节,随着 Swift 版本更新不会出现兼容性问题。
  6. 可调试性强:可以查看宏展开后的代码,便于理解和调试。
  7. 可扩展性:可以组合使用不同的宏,构建复杂的编解码逻辑。

ReerCodable 登场:化繁为简的魔法

ReerCodable 利用 Swift Macros 的强大能力,让你只需在类型或属性前添加简单的注解,就能自动生成高效、健壮的 Codable 实现。核心就是 @Codable 宏,它会与其他 ReerCodable 提供的宏协同工作,生成最终的编解码逻辑。框架接入支持 Cocoapods, SwiftPackageManager。

代码实现上参考了优秀的 winddpan/CodableWrapper、GottaGetSwifty/CodableWrappers 和 MetaCodable,相对它们 ReerCodable 有更丰富的 feature 或更简洁的使用。

让我们来看看 ReerCodable 如何优雅地解决上述痛点:

1. 自定义 CodingKey

通过 @CodingKey 可以为属性指定自定义 key,无需手动编写 CodingKeys 枚举:

ReerCodable Codable
swift
代码解读
复制代码
@Codable struct User { @CodingKey("user_name") var name: String @CodingKey("user_age") var age: Int var height: Double }
swift
代码解读
复制代码
struct User: Codable { var name: String var age: Int var height: Double enum CodingKeys: String, CodingKey { case name = "user_name" case age = "user_age" case height } }

2. 嵌套 CodingKey

支持通过点语法表示嵌套的 key path:

swift
代码解读
复制代码
@Codable struct User { @CodingKey("other_info.weight") var weight: Double @CodingKey("location.city") var city: String }

3. 多键解码

可以指定多个 key 用于解码,系统会按顺序尝试解码直到成功:

swift
代码解读
复制代码
@Codable struct User { @CodingKey("name", "username", "nick_name") var name: String }

4. 命名转换

支持多种命名风格转换,可以应用在类型或单个属性上:

swift
代码解读
复制代码
@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
swift
代码解读
复制代码
@Codable @CodingContainer("data.info") struct UserInfo { var name: String var age: Int }
json
代码解读
复制代码
{ "code": 0, "data": { "info": { "name": "phoenix", "age": 33 } } }

6. 编码专用 key

可以为编码过程指定不同的键名, 由于 @CodingKey 可能有多个参数, 再加上可以使用 @SnakeCase, KebabCase 等, 解码可能使用多个 key, 那编码时会采用第一个 key, 也可以通过 @EncodingKey 来指定 key

swift
代码解读
复制代码
@Codable struct User { @CodingKey("user_name") // 解码使用 "user_name", "name" @EncodingKey("name") // 编码使用 "name" var name: String }

7. 默认值支持

解码失败时可以使用默认值, 原生 Codable 针对非 Optional 属性, 会在没有解析到正确值时抛出异常导致整个 model 解码失败, 即使已经设置了初始值, 或者即使是 Optional 类型的枚举

swift
代码解读
复制代码
@Codable struct User { var age: Int = 33 var name: String = "phoenix" // 若 JSON 中 gender 字段不是 `male` 或 `female`, 原生 Codable 会抛出异常, ReerCodable 不会, 会设置其为 nil, 如 {"gender": "other"}, 可能出现在客户端定义了枚举, 但服务端新增了字段的业务场景 var gender: Gender? } @Codable enum Gender: String { case male, female }

8. 忽略属性

使用 @CodingIgnored 在编解码过程中忽略特定属性. 在解码过程中对于非 Optional 属性要有一个默认值才能满足 Swift 初始化的要求, ReerCodable 对基本数据类型和集合类型会自动生成默认值, 如果是其他自定义类型, 则需用用户提供默认值.

swift
代码解读
复制代码
@Codable struct User { var name: String @CodingIgnored var ignore: Set<String> }

9. Base64 编解码

自动处理 base64 字符串与 Data, [UInt8] 类型的转换:

swift
代码解读
复制代码
@Codable struct User { @Base64Coding var avatar: Data @Base64Coding var voice: [UInt8] }

10. 集合类型解码优化

使用 @CompactDecoding 在解码数组时自动过滤 null 值, 与 compactMap 是相同的意思:

swift
代码解读
复制代码
@Codable struct User { @CompactDecoding var tags: [String] // ["a", null, "b"] 将被解码为 ["a", "b"] }

同时, Dictionary 和 Set 也支持使用 @CompactDecoding 来优化

11. 日期编解码

支持多种日期格式的编解码:

ReerCodable JSON
swift
代码解读
复制代码
@Codable class DateModel { @DateCoding(.timeIntervalSince2001) var date1: Date @DateCoding(.timeIntervalSince1970) var date2: Date @DateCoding(.secondsSince1970) var date3: Date @DateCoding(.millisecondsSince1970) var date4: Date @DateCoding(.iso8601) var date5: Date @DateCoding(.formatted(Self.formatter)) var date6: Date static let formatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS" dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) return dateFormatter }() }
json
代码解读
复制代码
{ "date1": 1431585275, "date2": 1731585275.944, "date3": 1731585275, "date4": 1731585275944, "date5": "2024-12-10T00:00:00Z", "date6": "2024-12-10T00:00:00.000" }

12. 自定义编解码逻辑

通过 @CustomCoding 实现自定义的编解码逻辑. 自定义编解码有两种方式:

  • 通过闭包, 以 decoder: Decoder, encoder: Encoder 为参数来实现自定义逻辑:
swift
代码解读
复制代码
@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 协议的自定义类型来实现自定义逻辑:
swift
代码解读
复制代码
// 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 }

自定义实现过程中, 框架提供的方法也可以使编解码更加方便:

swift
代码解读
复制代码
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

swift
代码解读
复制代码
@Codable class Animal { var name: String } @InheritedCodable class Cat: Animal { var color: String }

14. 枚举支持

为枚举提供丰富的编解码能力:

  • 对基本枚举类型, 以及 RawValue 枚举支持
swift
代码解读
复制代码
@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
swift
代码解读
复制代码
@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 支持的, 即枚举值和其关联值是父子级的结构:
    swift
    代码解读
    复制代码
    @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() 进行自定义路径值的匹配
    swift
    代码解读
    复制代码
    @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. 生命周期回调

支持编解码的生命周期回调:

swift
代码解读
复制代码
@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 字符串和字典转换方法:

swift
代码解读
复制代码
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. 基本类型转换

支持基本数据类型之间的自动转换:

swift
代码解读
复制代码
@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 类型的编解码:

swift
代码解读
复制代码
@Codable struct Response { var data: AnyCodable // 可以存储任意类型的数据 var metadata: [String: AnyCodable] // 相当于[String: Any]类型 }

19. 生成默认实例

swift
代码解读
复制代码
@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? }

会生成以下实例

swift
代码解读
复制代码
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

swift
代码解读
复制代码
@Codable struct NetResponse<Element: Codable> { let data: Element? let msg: String private(set) var code: Int = 0 }

20. 生成 copy 方法

使用 Copyable 为模型生成 copy 方法

swift
代码解读
复制代码
@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, 还可以对部分属性进行更新

swift
代码解读
复制代码
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 为准

注:本文转载自juejin.cn的phoenix的文章"https://juejin.cn/post/7491231734954016818"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

141
iOS
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top