Swift JSON/Model库调研
近期(2023年11月)对Swift JSON与Model互转的代码库做了一点调研,希望找到好用的工具
方针
处理Swift原生Codable几个不易用的问题
需求阐明一点,下面罗列的一切问题,原生Codable都是能够处理的,仅仅做不到简易的要求,需求开发者手动写一些代码来完成
- 类型不匹配或JSON字段缺失导致编解码失利
- 默许情况下,运用Swift Codable时,假如一旦JSON数据中某个字段的类型与Model的特点类型不匹配,或许JSON中的值为null或缺失,则整个Model的编解码都会失利
- 咱们希望单个字段的缺少或类型不匹配不影响整个编解码过程
- 不易类型兼容
- 此处所说的类型兼容意思是,JSON值和Model对应字段类型不匹配但能够兼容时,比方Model要求bool类型,但返回值是int类型时,是能够进行兼容解析的
- 但默许情况下,原生Codable会解析失利
- 不支撑多CodingKey:JSON Key与Model.property的联系,有时是多对一的
- 即一个Model或许用于不同场景下,不同场景下或许拿到的JSON数据中字段名并不相同
- 无法简易供给默许值:无法简洁地为Mode的特点供给默许值,目前只能重写init来完成,且此时需求在init中为一切特点编写赋值逻辑,会多出一些重复工作
- 无法简易地自定义Transform
- 无法简易解析嵌套key
确保Model的封装性
- 封装性为面向目标规划思维中的四个特点之一,即削减露出目标的特点(成员变量)的存取权限,避免随意更改进而增加出问题风险
- 详细到JSON框架当中,便是能否将特点标记为
let
,假如能够那便是严厉确保了封装性 - 假如只能做到
private(set) var
则只能说保存部分封装性(其实本质上仍不是真正意义上的封装性) - 假如只能标记为
var
则完全不能确保封装性
- 详细到JSON框架当中,便是能否将特点标记为
- 之前在运用的JSON库虽处理了原生Codable的不易用问题,但或多或少存在违反规划原则问题,比方CodableWrapper根据PropertyWrapper完成,该feature要求每个特点必定是可变(var)的,这影响了目标的封装性
JSON库对比
ObjectMapper
- 原理:根据协议自定义编解码过程,不依靠于原生Codable,不依靠反射
- 原生Codable出来之前,运用的比较多
class User: Mappable {
var username: String?
var age: Int?
var weight: Double!
var array: [Any]?
var dictionary: [String : Any] = [:]
var bestFriend: User? // Nested User object
var friends: [User]? // Array of Users
var birthday: Date?
required init?(map: Map) {
}
// Mappable
func mapping(map: Map) {
username <- map["username"]
age <- map["age"]
weight <- map["weight"]
array <- map["arr"]
dictionary <- map["dict"]
bestFriend <- map["best_friend"]
friends <- map["friends"]
birthday <- (map["birthday"], DateTransform())
}
}
let user = User(JSONString: JSONString)
HandyJSON
- 自定义编解码过程,不依靠原生Codable
- 详细原理:从类信息里获取一切特点的特征,包含名称,特点在内存里的偏移量、特点的个数、特点的类型等等,然后将服务端返回来的数据用操作内存的方式将数值写入对应的内存,来完成json 转model
- 需求留意的是它激烈依靠 Metadata 结构,随着Swift版别和编译器的升级,这个结构随时或许有各种改变,容易引起溃散等不稳定问题
- 阿里出品
class Cat: HandyJSON {
var id: Int64!
var name: String!
var parent: (String, String)?
var friendName: String?
required init() {}
func mapping(mapper: HelpingMapper) {
// specify 'cat_id' field in json map to 'id' property in object
mapper <<<
self.id <-- "cat_id"
// specify 'parent' field in json parse as following to 'parent' property in object
mapper <<<
self.parent <-- TransformOf<(String, String), String>(fromJSON: { (rawString) -> (String, String)? in
if let parentNames = rawString?.characters.split(separator: "/").map(String.init) {
return (parentNames[0], parentNames[1])
}
return nil
}, toJSON: { (tuple) -> String? in
if let _tuple = tuple {
return "(_tuple.0)/(_tuple.1)"
}
return nil
})
// specify 'friend.name' path field in json map to 'friendName' property
mapper <<<
self.friendName <-- "friend.name"
}
}
let jsonString = "{"cat_id":12345,"name":"Kitty","parent":"Tom/Lily","friend":{"id":54321,"name":"Lily"}}"
if let cat = Cat.deserialize(from: jsonString) {
print(cat.id)
print(cat.parent)
print(cat.friendName)
}
KakaJSON
- 原理和HandyJSON相似,也是根据Swift的Metadata结构,经过读写数据结构内存完成编解码
- 作者是MJRefresh的作者小码哥
struct Cat: Convertible {
var name: String = ""
var weight: Double = 0.0
}
// json can also be NSDictionary, NSMutableDictionary
let json: [String: Any] = [
"name": "Miaomiao",
"weight": 6.66
]
let cat1 = json.kj.model(Cat.self)
// jsonData can alse be NSData, NSMutableData
let jsonData = """{ "name": "Miaomiao", "weight": 6.66}""".data(using: .utf8)!
let cat1 = jsonData.kj.model(Cat.self)
let cat2 = model(from: jsonData, Cat.self)
元编程计划
元编程(英语:Metaprogramming),又译超编程,是指某类计算机程序的编写,这类计算机程序编写或许操纵其它程序(或许本身)作为它们的材料,或许在编译时完成部分本应在运行时完成的工作。大都情况下,与手艺编写悉数代码比较,程序员能够获得更高的工作效率,或许给与程序更大的灵敏度去处理新的情形而无需重新编译。–来自维基百科
原理:因为原生Codable能力满意强大,所以元编程在JSON部分的应用主要体在,根据原生Codable,经过元编程计划协助咱们主动生成繁琐且无多大意义的编解码代码
经过下面演示大概感受一下Sourcery元编程计划完成的主动生成Codable代码的效果
-
调研到的元编程计划有两种:Swift Macros和Sourcery
-
SwiftMacros是Swift 5.9版别官方推出的feature;因为Swift是开源的(包含其AST),所以Sourcery是根据Swift开发的经过剖析AST,主动生成代码工具
-
元编程计划优点
- 本质上是协助开发者弥补胶水代码,稳定性有确保
-
SwiftMacros计划优于Sourcery
- SwiftMacros相似其他语言中的宏,但功用要丰富很多
- 能够经过开发自定义的宏,主动生成Codable中繁琐的代码,且支撑多种不同类型的宏,能够轻松打开、折叠宏,支撑编译时校验、断点调试宏,宏安全方面也经过诸多约束得以确保
- CodableWrapper从1.0.0版别开始用SwiftMacros进行了重写,是一种能够直接运用的元编程计划
-
SwiftMacros缺陷
- 只支撑SPM方式接入,不支撑Cocoapods,当然,即使运用Cocoapods也有方法运用宏,便是略微费事
-
Sourcery计划缺陷
- 该计划需求自定义代码生成规矩和模板,并且或许要求开发人员在指定要生成的代码时,恪守必定的书写标准,仍是有一点了解本钱
ExCodable
- 原理:经过承继原生Codable协议,在Codable编解码过程前后和过程中增加自定义逻辑,并经过PropertyWrapper特性来精简开发者代码量。思维来自Codextended(1.5k star)
- 悉数运用官方public API(有运用反射,但也仅运用了其public API进行读操作),没有猜想Metadata结构和进行内存操作
- 代码精简,一个文件,500+行
class ABCModel: Codable {
@ExCodable("abc.a", "isOn")
private(set) var boolValue: Bool = false
@ExCodable("str", decode: { decoder in
return "xxx"
})
private(set) var string: String? = nil
required init(from decoder: Decoder) throws {
try decode(from: decoder, nonnull: false, throws: false)
}
func encode(to encoder: Encoder) throws {
try encode(to: encoder, nonnull: false, throws: false)
}
}
CodableWrapper
- .0.0版别之前,经过反射、内存操作、PropertyWrapper等技能,在Codable编解码过程前后和过程中增加自定义逻辑。但因为存在内存指针操作和依靠Metadata结构的原因,存在不稳定因素
- 工程中已经验证,0.1.2版别的代码,在iOS升级到17后,运用体系原生JSONDecoder解码时,会导致溃散
- 1.0.0版别开始,运用Swift Macros重写,因为运用的是Swift官方支撑的能力,所以稳定性不会有问题
1.0.0版别代码演示
@Codable
struct BasicModel {
var defaultVal: String = "hello world"
var defaultVal2: String = Bool.random() ? "hello world" : ""
let strict: String
let noStrict: String?
let autoConvert: Int?
@CodingKey("hello")
var hi: String = "there"
@CodingNestedKey("nested.hi")
@CodingTransformer(StringPrefixTransform("HELLO -> "))
var codingKeySupport: String
@CodingNestedKey("nested.b")
var nestedB: String
var testGetter: String {
nestedB
}
}
0.3.3版别代码演示
class CodableWrapperModel: Codable {
Codec(transformer: SecondDateTransform())
var registerDate: Date?
@Codec("abc.a")
private(set) var int: Bool = false
@Codec("str, text")
private(set) var string: String? = nil
}
归纳对比
特性 | 原生Codable | ObjectMapper | HandyJSON | KakaJSON | ExCodable | CodableWrapper(0.3.3) | SwiftMacros计划 如CodableWrapper(1.0.0) | 根据 Sourcery 的计划 |
---|---|---|---|---|---|---|---|---|
类型不匹配或JSON字段缺失时不会导致编解码失利 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
类型兼容 | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
多CodingKey | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
简易供给默许值 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
简易解析嵌套key | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
简易自定义Transform | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
坚持封装性 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
稳定性 | ⭐️⭐️ | ⭐️⭐️ | ⭐️ | ⭐️ | ⭐️⭐️ | ⭐️ | ⭐️⭐️ | ⭐️⭐️ |
接入本钱 | easy | middle | middle | middle | easy | easy | middle | hard |
最低iOS版别要求 | 8 | 10 | 8 | 8 | 9 | 11 | 13 | 8 |
Star | – | 9.1k | 4.2k | 1.1k | 0.1k | 0.2k | 0.2k | 7.3k |
结论
汇总前面多个计划,个人最引荐SwiftMacros计划(比方1.0.0版别的CodableWrapper),其次是ExCodable。原因如下:
- CodableWrapper契合一切的方针要求,唯一缺陷是因为SwiftMacros功用比较新,有必定接入本钱,有必要满意如下要求
- 项目支撑SPM(初步验证SPM和Cocoapods能够一起运用)或用代替方法(将CodableWrapper运用Cocoapods发布)
- 工程最低支撑的iOS体系版别有必要大于等于13,最低Swift版别大于等于5.9(对应Xcode版别为15)
- 将ExCodable作为备选是因为,它能满意绝大部分场景要求,且代码最为精简,无trick行为稳定性有确保