从 HandyJSON 迁移到 SmartCodable:我们团队的实践

本文记录了我们团队将一个 10 万行 Swift 项目从 HandyJSON 迁移到 SmartCodable 的完整过程,包括迁移动机、踩过的坑、API 对照表,以及迁移后的效果对比。如果你的项目还在用 HandyJSON,希望这篇文章能帮你做出判断。

一、为什么要迁移

HandyJSON 的定时炸弹

HandyJSON 是国内 iOS 社区广泛使用的 JSON 解析库。它的优点很明显------API 简洁,支持 Any 类型,支持继承,几乎不需要额外的模板代码。我们团队用了两年多,一直没出什么问题。

直到 Swift 5.5 引入结构化并发之后,问题开始浮现。

HandyJSON 的核心实现依赖 Swift 运行时的内存布局反射------直接读取 struct/class 的内存 metadata,计算属性偏移量,然后写入值。这个机制有两个致命问题:

  1. 不是官方支持的 API 。Swift 的内存布局在不同版本之间没有 ABI 稳定性承诺。Apple 每次更新 Swift 版本,都有可能改变 metadata 的结构,导致 HandyJSON 静默地写错内存位置。这不会崩溃,而是静默返回错误的数据------更危险。
  2. 与 Swift 并发模型冲突。Swift 5.5+ 的并发检查越来越严格,HandyJSON 的运行时反射无法被标记为 Sendable,在启用严格并发检查的项目中会产生大量警告。

我们在一次 Xcode 15 升级后遇到了一个诡异的 Bug:某个嵌套模型的属性偶尔解析为零值。排查了两天才发现是 HandyJSON 的内存偏移计算在新版 Swift 编译器下出了问题。这次事件让我们决定迁移。

为什么选择 SmartCodable

我们评估了三个方案:

方案 优点 缺点
手写 init(from:) 零依赖,完全可控 样板代码爆炸,100 个 Model 就是地狱
CodableWrapper / BetterCodable 轻量,只用属性包装器 只解决默认值问题,不解决类型转换、Any 支持
SmartCodable 功能对齐 HandyJSON,基于原生 Codable 学习成本低,API 设计与 HandyJSON 相似

SmartCodable 胜出的原因很简单:它是唯一一个在功能上能完全替代 HandyJSON 的方案,同时又基于 Apple 原生 Codable 协议,没有运行时安全隐患。


二、迁移前的准备

评估工作量

我们先做了一次全局搜索,统计 HandyJSON 的使用范围:

bash 复制代码
 # 统计引用 HandyJSON 的文件数
 grep -rl "HandyJSON" --include="*.swift" . | wc -l
 ​
 # 统计 deserialize 调用次数
 grep -rn "deserialize(from:" --include="*.swift" . | wc -l
 ​
 # 统计 mapping 方法使用次数
 grep -rn "mapping(mapper:" --include="*.swift" . | wc -l
 ​
 # 统计 Any 类型属性
 grep -rn "var.*: Any" --include="*.swift" . | wc -l

我们的项目情况:

  • 约 200 个 Model 文件
  • 80+ 处 deserialize 调用
  • 15 处 mapping(mapper:) 自定义映射
  • 8 处 Any 类型属性
  • 3 处继承关系

制定迁移策略

根据评估结果,我们制定了分步迁移策略:

  1. 第一步:全局替换协议名(工作量最大但最简单)
  2. 第二步 :处理 mapping 方法(需要逐个改写)
  3. 第三步 :处理 Any 类型属性(加 @SmartAny
  4. 第四步 :处理继承关系(加 @SmartSubclass
  5. 第五步 :处理枚举(HandyJSONEnumSmartCaseDefaultable
  6. 第六步 :处理序列化(toJSON()toDictionary()
  7. 第七步:全量测试

三、逐步迁移

第一步:替换协议名(5 分钟)

这是最简单的一步,全局搜索替换即可:

arduino 复制代码
 import HandyJSON → import SmartCodable
 HandyJSON         → SmartCodable       (作为协议名使用的地方)

SmartCodable 的 deserialize(from:) API 与 HandyJSON 完全一致,所以替换协议名后,所有反序列化代码不需要改动。

csharp 复制代码
 // HandyJSON(替换前)
 guard let model = Model.deserialize(from: dict) else { return }
 ​
 // SmartCodable(替换后)------ 调用方式完全一样
 guard let model = Model.deserialize(from: dict) else { return }

唯一的小差异 :HandyJSON 解码数组时返回 [Model]?,有些地方写了 as? [Model] 强转。SmartCodable 不需要这个强转,但保留也不会报错,可以后续清理。

csharp 复制代码
 // HandyJSON 写法
 guard let models = [Model].deserialize(from: arr) as? [Model] else { return }
 ​
 // SmartCodable 写法(as? [Model] 可以删掉,不删也没问题)
 guard let models = [Model].deserialize(from: arr) else { return }

第二步:改写自定义映射(30 分钟)

这是工作量最大的一步。HandyJSON 用 mapping(mapper:) 方法,SmartCodable 用 mappingForKey(),语法不同:

HandyJSON:

swift 复制代码
 struct Model: HandyJSON {
     var nickName: String = ""
     var userAge: Int = 0
     var ignoreField: String = ""
 ​
     mutating func mapping(mapper: HelpingMapper) {
         mapper <<< self.nickName <-- ["nick_name", "realName"]
         mapper <<< self.userAge <-- "user_age"
         mapper >>> self.ignoreField   // 忽略该字段
     }
 }

SmartCodable:

swift 复制代码
 struct Model: SmartCodable {
     var nickName: String = ""
     var userAge: Int = 0
     @SmartIgnored
     var ignoreField: String = ""
 ​
     static func mappingForKey() -> [SmartKeyTransformer]? {
         [
             CodingKeys.nickName <--- ["nick_name", "realName"],
             CodingKeys.userAge <--- "user_age"
         ]
     }
 }

对照表:

HandyJSON SmartCodable 说明
mapper <<< self.prop <-- "key" CodingKeys.prop <--- "key" 单字段映射
mapper <<< self.prop <-- ["k1", "k2"] CodingKeys.prop <--- ["k1", "k2"] 多候选映射
mapper >>> self.prop @SmartIgnored var prop 忽略字段

踩坑提醒 :SmartCodable 的 mappingForKey()static func,不是 mutating func。如果你的 mapping 中有依赖 self 的逻辑,需要调整。

第三步:处理 Any 类型(10 分钟)

HandyJSON 天然支持 Any 类型,SmartCodable 需要加 @SmartAny 属性包装器:

less 复制代码
 // HandyJSON
 struct Model: HandyJSON {
     var extra: [String: Any] = [:]
     var tags: [Any] = []
     var value: Any?
 }
 ​
 // SmartCodable
 struct Model: SmartCodable {
     @SmartAny var extra: [String: Any] = [:]
     @SmartAny var tags: [Any] = []
     @SmartAny var value: Any?
 }

全局搜索 var.*: Anyvar.*: [Any]var.*: [String: Any],逐个加上 @SmartAny 即可。

第四步:处理继承(5 分钟)

HandyJSON 自动处理继承,SmartCodable 需要在子类上加 @SmartSubclass

kotlin 复制代码
 // HandyJSON ------ 什么都不用加
 class BaseModel: HandyJSON {
     var name: String = ""
     required init() {}
 }
 class SubModel: BaseModel {
     var age: Int = 0
 }
 ​
 // SmartCodable ------ 子类加 @SmartSubclass
 class BaseModel: SmartCodable {
     var name: String = ""
     required init() {}
 }
 @SmartSubclass
 class SubModel: BaseModel {
     var age: Int = 0
 }

注意@SmartSubclass 是 Swift 宏,需要 Swift 5.9+ 和 Xcode 15+。如果你的项目还在用低版本,可以参考 低版本继承方案

第五步:处理枚举(5 分钟)

arduino 复制代码
 // HandyJSON
 enum Sex: String, HandyJSONEnum {
     case man
     case woman
 }
 ​
 // SmartCodable
 enum Sex: String, SmartCaseDefaultable {
     case man
     case woman
 }

全局替换 HandyJSONEnumSmartCaseDefaultable

第六步:处理序列化(10 分钟)

序列化的 API 名称有变化:

HandyJSON SmartCodable
model.toJSON() model.toDictionary()
model.toJSONString() model.toJSONString()
models.toJSON() models.toArray()
models.toJSONString() models.toJSONString()

全局搜索 .toJSON() 替换为 .toDictionary()(注意排除 toJSONString)。数组序列化搜索替换即可。

第七步:全量测试

移除 HandyJSON 依赖,编译通过后进行全量测试。

我们的测试策略

  1. 先开启 SmartSentinel 日志,跑一遍主流程:
ini 复制代码
 SmartSentinel.debugMode = .verbose
  1. 观察日志中是否有异常的类型转换或缺失字段
  2. 重点验证有 mapping 的模型、有 Any 类型的模型、有继承的模型
  3. 回归测试核心业务流程

四、迁移后的收获

解析异常不再是黑盒

HandyJSON 解析失败时,你只知道"解析返回了 nil",但不知道哪个字段出了问题。SmartCodable 的 SmartSentinel 日志系统会精确告诉你:

vbnet 复制代码
 ================================ [Smart Sentinel] ================================
 UserModel 👈🏻 👀
 ╆━ UserModel
 ┆┄ age       : Expected Int, got String --- auto-converted
 ┆┄ email     : Key not found --- using default ""
 ====================================================================================

我们在迁移后第一周就通过 Sentinel 日志发现了 3 个后端接口返回类型不一致的问题------这些问题在 HandyJSON 时代被静默吞掉了。

告别运行时崩溃的恐惧

HandyJSON 的每次 Swift 版本升级都是一次赌博。SmartCodable 基于原生 Codable,Swift 版本升级时完全不需要担心底层兼容性。

类型转换更智能

SmartCodable 内置的类型转换比 HandyJSON 更全面:

ini 复制代码
 // 后端返回 String 类型的 "123",Model 声明为 Int
 var age: Int = 0
 // HandyJSON: age = 0(转换失败,静默用默认值)
 // SmartCodable: age = 123(自动转换成功)

编译速度

移除 HandyJSON 后,项目的 clean build 时间减少了约 8%(HandyJSON 的运行时反射代码量较大)。


五、迁移清单(Checklist)

供你在实际迁移时对照使用:

  • 全局替换 import HandyJSONimport SmartCodable
  • 全局替换协议名 HandyJSONSmartCodable(注意只替换作为协议使用的)
  • 改写所有 mapping(mapper:)mappingForKey() + @SmartIgnored
  • 所有 Any / [Any] / [String: Any] 属性加 @SmartAny
  • 所有子类加 @SmartSubclass
  • 全局替换 HandyJSONEnumSmartCaseDefaultable
  • 全局替换 .toJSON().toDictionary()(排除 toJSONString
  • 数组序列化 .toJSON().toArray()
  • 移除 HandyJSON 依赖(Podfile / Package.swift)
  • 编译通过
  • 开启 SmartSentinel.debugMode = .verbose,跑主流程
  • 全量回归测试
  • 关闭 Sentinel(SmartSentinel.debugMode = .none
  • 上线观察

六、总结

整个迁移过程比我们预想的顺利很多。200 个 Model 的项目,两个人花了一天半 完成迁移和测试。其中 80% 的工作量是全局替换(第一步),真正需要手动处理的只有 mapping 改写和 @SmartAny 标注。

如果你的项目还在用 HandyJSON,我的建议是:不要等到被 Swift 版本升级逼着迁移,主动迁移的成本远低于被动修 Bug。

SmartCodable 的 API 设计明显考虑了 HandyJSON 用户的迁移体验------deserializedidFinishMappingdesignatedPath 这些核心 API 完全一致,迁移门槛很低。

相关推荐
kerli3 小时前
基于 kmp/cmp 的跨平台图片加载方案 - 适配 Android View/Compose/ios
android·前端·ios
懋学的前端攻城狮5 小时前
第三方SDK集成沉思录:在便捷与可控间寻找平衡
ios·前端框架
冰凌时空8 小时前
Swift vs Objective-C:语言设计哲学的全面对比
ios·openai
花间相见9 小时前
【大模型微调与部署03】—— ms-swift-3.12 命令行参数(训练、推理、对齐、量化、部署全参数)
开发语言·ios·swift
SameX9 小时前
删掉ML推荐、砍掉五时段分析——做专注App时我三次推翻自己,换来了什么
ios
YJlio11 小时前
2026年4月19日60秒读懂世界:从学位扩容到人形机器人夺冠,今天最值得关注的6个信号
python·安全·ios·机器人·word·iphone·7-zip
90后的晨仔1 天前
《SwiftUI 高级特性第1章:自定义视图》
ios
空中海1 天前
第二章:SwiftUI 视图基础
ios·swiftui·swift
空中海1 天前
第七章:iOS网络与数据持久化
网络·ios