NovaCodable 是一款基于 Swift Macro(编译期代码生成)的高性能、轻量级、零反射的 JSON 序列化与反序列化基建库。它通过在编译期静态生成平铺的官方 Codable 代码,彻底消灭了运行时反射带来的性能开销,同时针对 Swift 编译器进行了 AST 树深度优化,大幅提升了大型项目中的类型推断与编译速度。
本文件旨在提供一份标准的通用开发规范,引导业务层正确、高效地编写数据模型。
- 零侵入与弱耦合:业务模型无需继承特定基类,通过
@Codable装饰即可自动获得完整的原生产能。 - 静态平铺提速:底层摒弃了深层泛型嵌套、去除了
??(空合运算符)的类型推断耗时,编译速度无限逼近纯手写代码。 - 智能类型容错:内置
decodeSmart引擎,支持常用基础类型的无感自动转换(例如:后端返回Int/Double,模型定义为String时,会自动转为对应字符串,绝不因类型不匹配导致整个模型白屏或崩溃)。 - 安全防崩溃:重写可选值编码逻辑,当值为
nil时直接剔除 Key,彻底断绝包含<null>(NSNull)的脏字典写入UserDefaults时引发的线上 Crash。
以下以日常业务最频繁的用户信息模型为例,全面展示标准字段、字面量默认值、可选型、自定义映射、枚举兜底以及模型嵌套的推荐写法。
Swift
import Foundation
import NovaCodable
// ==========================================
// 辅助模型 1:安全枚举兜底
// ==========================================
@CodableEnum(fallback: "unknown")
public enum UserGender: String {
case male = "MALE"
case female = "FEMALE"
case unknown = "UNKNOWN" // 当后端返回未知的状态(如 "OTHER")时,安全兜底为此处
}
// ==========================================
// 辅助模型 2:被嵌套的勋章模型
// ==========================================
@Codable
public class UserMedalDTO {
public var medalId: String = ""
public var medalName: String = ""
public var iconUrl: String = ""
}
// ==========================================
// 主模型:用户信息响应体
// ==========================================
@Codable
public class UserInfoResponse {
/// 用户唯一标识符 (字符串类型)
public var id: String = ""
/// 用户业务Code (支持智能容错:若后端返回数字 20001630,会自动转为 "20001630")
public var code: String = ""
/// 用户昵称 (字面量默认值,若JSON中缺失该字段,则保留此处默认值)
public var nickname: String = "未命名用户"
/// 用户头像 (可选型,允许为 nil)
public var avatar: String? = nil
/// 地区码 (如 "TR")
public var areaCode: String = ""
/// 国家码 (自定义Key映射:将响应体中的 "country_code" 映射到该属性)
@SerializedName("country_code")
public var countryCode: String = ""
/// 用户性别 (内置枚举类型,安全容错)
public var gender: UserGender = .unknown
/// 用户勋章列表 (模型嵌套 + 可选型数组)
public var medalsV2: [UserMedalDTO]? = nil
/// 隐藏的敏感字段/本地临时字段 (不参与任何 JSON 解析与编码)
@CodableIgnore
public var localCacheTimestamp: Int = 0
public var thumbnails: [[String: AnyCodable]]?
}AnyCodable使用https://github.com/Flight-School/AnyCodable
- 字面量默认值(Default Values):强烈建议为所有必选基础字段(如
String,Int,Double,Bool)赋予初始字面量。当服务器返回的 JSON 缺失某些字段时,NovaCodable 会保留本地初始值,确保业务逻辑拿到的是安全可控的数据。 - 可选型(Optional
?):只有确定该字段允许为nil且业务有针对nil的专门判断(如控制某些 UI 组件的显示与隐藏)时,才建议使用?。 - 智能转换验证:上述例子中,即使服务器下发的
code是整型12345,业务层无需修改模型,底层重载的decodeSmart会在内存组装时默默将其安全的格式化为"12345"。 - 属性忽略(
@CodableIgnore):常用于页面打点、本地计时器、或是状态暂存字段,带上此标签后,编码和解码都会完全无视它。
当业务模型存在复杂的父子继承结构时,基类与子类必须分工明确。子类不再使用 @Codable,而是显式通过 @CodableSubclass 声明,宏会自动补齐子类所必须的 super.init() 及 override encode(to:) 派发。
Swift
/// 1. 基础事件基类
@Codable
public class LMRoomBaseEvent {
public var eventId: String = ""
public var createTime: Int = 0
public init() {} // 显式提供给子类 super 调用
}
/// 2. 具体的礼物事件子类
@CodableSubclass
public class LMRoomGiftEvent: LMRoomBaseEvent {
public var giftId: String = ""
public var giftName: String = ""
public var giftNum: Int = 1
public override init() {
super.init()
}
}当后端下发的数据格式与客户端纯净的业务模型内存类型不一致,且该转换极其复杂或影响列表滑动性能时,推荐使用轻量级转换器将计算提前。
Swift
// 1. 实现通用转换器协议(全局编译一次,不增加宏开销)
public struct StringToIntArrayTransformer: NovaTransformer {
public typealias JSONType = String
public typealias ModelType = [Int]
// 解码:将后台返回的 "101,102,103" 在后台线程一次性切好存在内存
public static func transform(from json: String) -> [Int] {
return json.components(separatedBy: ",").compactMap { Int($0) }
}
// 编码:反向回退成字符串发给后台
public static func transform(to model: [Int]) -> String {
return model.map { "\($0)" }.joined(separator: ",")
}
}
// 2. 业务使用
@Codable
public class LMRoomPermissionModel {
/// 房间管理员ID列表(在解析瞬间完成转换,UI层直接无感读取高效数组,杜绝掉帧)
@Transform(StringToIntArrayTransformer.self)
public var administratorIds: [Int] = []
}默认情况下,宏会在底层为没有自定义构造器的类自动补充一个全字段入参的 init(id:...) 以及空的 init()。
若模型内有极其复杂的自定义初始化逻辑,或出于对大型响应体模型极限压缩代码量、缩短编译时间的目的(纯响应模型在客户端不需要手动 new 创建对象),可以手动关闭它。
Swift
@Codable(memberwiseInit: false) // 👈 明确告知宏:关闭自动 init 生成,权限完全交还手写
public class LMGiftBackpack {
public var categoryId: String = ""
public var categoryName: String = ""
// 手动补充精简的空构造器,供外部或子类调用
public init() {}
}为了保证迁移过程中老代码的绝对安全,不要直接大范围替换业务层的调用,可以通过框架层暴露的全局扩展网关,安全透明地接管项目的输入与输出。
Swift
// 转换示例:
let dict: [String: Any] = ["id": "63801", "code": 20001630]
// 🛠 解码 (deserialize)
if let userInfo = UserInfoResponse.deserialize(from: dict) {
print("解析成功,昵称:\(userInfo.nickname)")
}
// 🛠 编码 (toJSON)
if let cleanDict = userInfo.toJSON() {
// 此时 cleanDict 已经过底层递归洗白(removingNSNulls),不包含任何 NSNull
UserDefaults.standard.set(cleanDict, forKey: "LM_currentUser") // 💯 绝对不崩溃
}
业务研发进行新模型编写或旧模型重构时,请严格参考上述 UserInfoResponse 的标准进行声明,善用 @CodableEnum 替换原生的不稳定枚举。这能有效保障线上崩溃率维持在极低的水准,同时保障 App 的快速迭代构建。
Python
content = """# NovaCodable
NovaCodable 是一款基于 Swift Macro(编译期代码生成)的高性能、轻量级、零反射的 JSON 序列化与反序列化基建库。
它通过在编译期静态生成平铺的官方 `Codable` 代码,彻底消灭了运行时反射带来的性能开销。同时针对 Swift 编译器进行了 AST 树深度优化,大幅提升了大型项目中的类型推断与编译速度。
本指南旨在帮助开发者**从零开始,快速上手并无缝迁移**现有业务模型。
---
## 一、 核心优势
1. **极致编译速度**:底层摒弃深层泛型嵌套,编译速度无限逼近纯手写原生代码,告别宏展开卡顿。
2. **零侵入与弱耦合**:无需继承特定的基类,只需打上 `@Codable` 标签即可自动获得解析能力。
3. **智能类型容错**:内置强悍的容错引擎,支持基础类型无感转换(如:后端下发 `Int` 123,模型定义为 `String` 时,自动转为 `"123"`)。
4. **安全防崩溃**:重写可选值编码逻辑,自动剔除 `nil` 值的 Key,彻底告别旧字典写入 `UserDefaults` 时由 `<null>` 引发的线上 Crash。
---
## 二、 🚀 快速迁移指南(1分钟上手)
如果你正在从 HandyJSON、SmartCodable 或其他第三方库迁移,只需遵循以下三个步骤。
### Step 1: 基础模型替换
去掉原有的继承或协议,直接在模型顶部添加 `@Codable` 宏。强烈建议为必选字段赋予**初始字面量默认值**。
Code output
README.md created successfully
```swift
import NovaCodable
@Codable
public class UserInfoResponse {
/// 正常字段:推荐赋予默认值
public var id: String = ""
public var nickname: String = "未命名用户"
/// 可选字段:允许为 nil 时使用 `?`
public var avatar: String? = nil
}
如果后端的 JSON 字段名(如 country_code)与客户端变量名(如 countryCode)不一致,使用 @SerializedName。
Swift
@Codable
public class UserInfoResponse {
@SerializedName("country_code")
public var countryCode: String = ""
}
如果某些字段仅用于本地缓存或页面状态,不需要参与网络 JSON 的解析与生成,使用 @CodableIgnore。
Swift
@Codable
public class UserInfoResponse {
@CodableIgnore
public var localCacheTimestamp: Int = 0
}
痛点:后端突然新增枚举类型,导致前端原生 Codable 解析失败,整个页面白屏崩溃。
解法:使用 @CodableEnum 并用 @CodableDefault 指定一个未知的兜底项。
Swift
@CodableEnum
public enum UserGender: String {
case male = "MALE"
case female = "FEMALE"
@CodableDefault
case unknown = "UNKNOWN" // 🛡 当后端返回未知的 "OTHER" 等状态时,安全回退到此处
}
当业务模型存在复杂的父子继承结构时,基类使用 @Codable,子类必须使用 @CodableSubclass。宏会自动补齐子类所需的 super.init() 调用。
Swift
/// 1. 基础事件基类
@Codable
public class LMRoomBaseEvent {
public var eventId: String = ""
public var createTime: Int = 0
public init() {} // 显式提供给子类 super 调用
}
/// 2. 具体的礼物事件子类
@CodableSubclass
public class LMRoomGiftEvent: LMRoomBaseEvent {
public var giftId: String = ""
public var giftName: String = ""
// 宏会自动生成全属性构造器并安全调用 super.init()
}
默认情况下,宏会自动在底层补充一个全字段入参的 init(id:...) 以及空的 init()。
如果你的模型有极其特殊的自定义初始化逻辑,或者想极致压缩代码量(如纯网络响应模型,无需手动实例化),可以手动关闭它。
Swift
@Codable(memberwiseInit: false) // 👈 明确告知宏:关闭自动 init 生成,权限完全交还手写
public class LMGiftBackpack {
public var categoryId: String = ""
public var categoryName: String = ""
// 手动补充精简的空构造器,供外部或子类调用
public init() {}
}