✨✨✨看起来还不错?给个star✨吧,急需支持✨✨✨
SmartCodable 是一个基于Swift的Codable协议的数据解析库,旨在提供更为强大和灵活的解析能力。通过优化和扩展Codable的标准功能,SmartCodable 有效地解决了传统解析过程中的常见问题,并提高了解析的容错性和灵活性。
在使用标准的Codable进行数据解析时,开发者常常会遇到诸如键不存在、类型不匹配或值为null等问题,这些问题往往会导致整个解析过程的失败,并抛出异常。SmartCodable 针对这些挑战提供了智能化的解决方案,确保解析过程的健壮性和流畅性。
- 增强的错误处理:当遇到键不存在、类型不匹配或值为null等问题时,不会立即中断解析,而是提供了更为灵活的处理选项。
- 值类型转换:如果目标类型与实际类型不符但可以进行有意义的转换,SmartCodable 会自动转换值类型,确保数据的正确解析。
- 默认值填充:当某个属性无法解析时,SmartCodable 允许自动填充该属性类型的默认值,例如将Bool类型的字段默认设为
false
,从而避免了整个解析过程的失败。 - 兼容性和灵活性:SmartCodable 完全兼容标准的Codable协议,并在此基础上提供更多的定制化选项,适应更复杂和多变的数据解析需求。
使用这样的数组,数组的元素项分别设置为: 100个,1000个,10000个。分别对这五种解析方案进行解析耗时的统计。
[
{
"name": "Anaa Airport",
"iata": "AAA",
"icao": "NTGA",
"coordinates": [-145.51222222222222, -17.348888888888887],
"runways": [
{
"direction": "14L/32R",
"distance": 1502,
"surface": "flexible"
}
]
}
]
理论上SmartCodable的解析效率是低于Codable的。如果不解析 runways ,就是如此。 SmartCodable对于枚举项的解析更加高效。所以在本次数据对比中,解析效率最高,甚至高于Codable。
Demo工程中提供了测试用例,请自行下载工程代码,访问 Tests.swift 文件。
Demo工程中提供了测试用例,请自行下载工程代码,访问 AreaTests.swift 文件。
Codable
和HandyJSON
是两种常用的方法。
-
HandyJSON 使用Swift的反射特性来实现数据的序列化和反序列化。该机制是非法的,不安全的, 更多的细节,可以访问 HandyJSON 的466号issue.
-
Codable 是Swift标准库的一部分,提供了一种声明式的方式来进行序列化和反序列化,它更为通用。
比较这两者在性能方面的差异需要考虑不同的数据类型和场景。一般而言,Codable
在以下情况下可能具有比HandyJSON
更低的解析耗时:
- 标准的JSON结构: 当解析标准且格式良好的JSON数据时,
Codable
通常表现出较好的性能。这是因为Codable
是Swift标准库的一部分,得到了编译器的优化。 - 复杂数据模型: 对于包含多层嵌套和复杂数据结构的JSON,
Codable
可能比HandyJSON
更有效,特别是在类型安全和编译时检查方面。 - 类型安全性高的场景:
Codable
提供了更强的类型安全性,这有助于在编译时捕捉错误。在处理严格遵循特定模型的数据时,这种类型检查可能带来性能优势。 - 与Swift特性集成:
Codable
与Swift的其他特性(如类型推断、泛型等)集成得更紧密,这可能在某些情况下提高解析效率。
然而,这些差异并不是绝对的。HandyJSON
在某些情况下(如处理动态或非结构化的JSON数据)可能表现得更好。性能也会受到JSON数据的大小和复杂性、应用的具体实现方式以及运行时环境等因素的影响。实际应用中,选择Codable
或HandyJSON
应基于具体的项目需求和上下文。
使用SmartCodable与使用标准的Codable类似,但你会获得额外的错误处理能力和更加灵活的解析选项。只需将你的数据模型遵循SmartCodable协议,即可开始享受更加智能的数据解析体验。
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!
target 'MyApp' do
pod 'SmartCodable'
end
import SmartCodable
struct Model: SmartCodable {
var name: String = ""
}
let dict: [String: String] = ["name": "xiaoming"]
guard let model = Model.deserialize(dict: dict) else { return }
import SmartCodable
struct Model: SmartCodable {
var name: String = ""
}
let dict: [String: String] = ["name": "xiaoming"]
let arr = [dict, dict]
guard let models = [Model].deserialize(array: arr) else { return }
// 字典转模型
guard let xiaoMing = JsonToModel.deserialize(dict: dict) else { return }
// 模型转字典
let studentDict = xiaoMing.toDictionary() ?? [:]
// 模型转json字符串
let json1 = xiaoMing.toJSONString(prettyPrint: true) ?? ""
// json字符串转模型
guard let xiaoMing2 = JsonToModel.deserialize(json: json1) else { return }
class Model: SmartDecodable {
var name: String = ""
var age: Int = 0
var desc: String = ""
required init() { }
// 解析完成的回调
func didFinishMapping() {
if name.isEmpty {
desc = "\(age)岁的" + "人"
} else {
desc = "\(age)岁的" + name
}
}
}
struct CompatibleEnum: SmartCodable {
init() { }
var enumTest: TestEnum = .a
enum TestEnum: String, SmartCaseDefaultable {
static var defaultCase: TestEnum = .a
case a
case b
case hello = "c"
}
}
让你的枚举遵守 SmartCaseDefaultable 协议,如果枚举解析失败,将使用defaultCase作为默认值。
Codable是无法解码Any类型的,这样就意味着模型的属性类型不可以是 Any,[Any],**[String: Any]**等类型, 这对解码造成了一定的困扰。
对非原生类型字段,给它再生成一个struct,用原生类型来表述属性就行。
struct Block: Codable {
let message: String
let index: Int
let transactions: [[String: Any]]
let proof: String
let previous_hash: String
}
改为:
struct Block: Codable {
let message: String
let index: Int
let transactions: [Transaction]
let proof: String
let previous_hash: String
}
struct Transaction: Codable {
let amount: Int
let recipient: String
let sender: String
}
如果情况允许,可以使用泛型来代替。
struct AboutAny<T: Codable>: SmartCodable {
init() { }
var dict1: [String: T] = [:]
var dict2: [String: T] = [:]
}
guard let one = AboutAny<String>.deserialize(dict: dict) else { return }
SmartAny 是SmartCodable 提供的解决Any的一个类型。可以直接像使用 Any 一样使用它。
struct AnyModel: SmartCodable {
var name: SmartAny?
var age: SmartAny = .int(0)
var dict: [String: SmartAny] = [:]
var arr: [SmartAny] = []
}
let inDict = [
"key1": 1,
"key2": "two",
"key3": ["key": "1"],
"key4": [1, 2.2]
] as [String : Any]
let arr = [inDict]
let dict = [
"name": "xiao ming",
"age": 20,
"dict": inDict,
"arr": arr
] as [String : Any]
guard let model = AnyModel.deserialize(dict: dict) else { return }
print(model.name)
// print: Optional(SmartAny.string("xiao ming"))
print(model.age)
// print: SmartAny.int(20)
print(model.dict)
// print:
[
"key1": SmartAny.int(1),
"key2": SmartAny.string("two"),
"key3": SmartAny.dict(["key": SmartAny.string("1")]),
"key4": SmartAny.array([SmartAny.int(1), SmartAny.double(2.2)])
]
print(model.arr)
// print:
[
SmartAny.dict([
"key1": SmartAny.int(1),
"key2": SmartAny.string("two")
"key3": SmartAny.dict(["key": SmartAny.string("1")]),
"key4": SmartAny.array([SmartAny.int(1), SmartAny.double(2.2)]),
])
]
可以看到打印的数据被SmartAny包裹住了,需要使用 .peel
去壳。
print(model.name?.peel)
print(model.age.peel)
print(model.dict.peel)
print(model.arr.peel)
JSONDecoder.SmartOption提供了三种解码选项,分别为:
public enum SmartOption {
/// 用于解码 “Date” 值的策略
case dateStrategy(JSONDecoder.DateDecodingStrategy)
/// 用于解码 “Data” 值的策略
case dataStrategy(JSONDecoder.DataDecodingStrategy)
/// 用于不符合json的浮点值(IEEE 754无穷大和NaN)的策略
case floatStrategy(JSONDecoder.NonConformingFloatDecodingStrategy)
}
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let option: JSONDecoder.SmartOption = .dateStrategy(.formatted(dateFormatter))
guard let model = FeedOne.deserialize(json: json, options: [option]) else { return }
let option: JSONDecoder.SmartOption = .dataStrategy(.base64)
guard let model = FeedOne.deserialize(json: json, options: [option]) else { return }
gurad let data = model.address, let url = String(data: data, encoding: .utf8) { else }
let option: JSONDecoder.SmartOption = .floatStrategy(.convertFromString(positiveInfinity: "infinity", negativeInfinity: "-infinity", nan: "NaN"))
guard let model1 = FeedOne.deserialize(json: json, options: [option]) else { return }
如果您需要将这样的数据结构
let dict: [String: Any] = [
"nick_name": "Mccc1",
"two": [
"realName": "Mccc2",
"three": [
["nickName": "Mccc3"]
]
]
]
解析到下面定义的Model中
struct FeedTwo: SmartCodable {
var nickName: String = ""
var two: Two = Two()
}
struct Two: SmartCodable {
var nickName: String = ""
var three: [Three] = []
}
struct Three: SmartCodable {
var nickName: String = ""
}
此时数据中字段名和Model中的属性名不一致,推荐您使用 CodingKeys。
struct FeedTwo: SmartCodable {
var nickName: String = ""
var two: Two = Two()
enum CodingKeys: String, CodingKey {
case nickName = "nick_name"
case two
}
}
struct Two: SmartCodable {
var nickName: String = ""
var three: [Three] = []
enum CodingKeys: String, CodingKey {
case nickName = "realName"
case three
}
}
struct Three: SmartCodable {
var nickName: String = ""
enum CodingKeys: String, CodingKey {
case nickName = "nick_name"
}
}
如果您有更复杂的需求,比如 多字段映射,重写CodingKeys无法满足。您可以使用提供的SmartDecodingKey来解决问题。
public enum SmartDecodingKey {
/// 使用默认key
case useDefaultKeys
/// 蛇形命名转换成驼峰命名
case convertFromSnakeCase
/// 自定义映射关系,会覆盖本次所有映射。
case globalMap([SmartGlobalMap])
/// 自定义映射关系,仅作用于path路径对应的映射。
case exactMap([SmartExactMap])
}
-
useDefaultKeys: 使用默认的解析映射方式。
-
convertFromSnakeCase: 蛇形命名转驼峰,覆盖本次解析。
-
globalMap:自定义解析映射,覆盖本次解析。
-
exactMap: 自定义解析映射,只影响提供路径下的解析映射。
let keys = [
SmartGlobalMap(from: "nick_name", to: "nickName"),
SmartGlobalMap(from: "realName", to: "nickName"),
]
guard let feedTwo = FeedTwo.deserialize(dict: dict, keyStrategy: .globalMap(keys)) else { return }
将数据中的 nick_name 字段映射到 模型的nickName 属性上。
需要注意的是:这个映射关系也会作用到嵌套的数据结构上。
如果你想避免上面的影响,可以使用 精准映射 。
let keys2 = [
SmartExactMap(path: "", from: "nick_name", to: "nickName"),
SmartExactMap(path: "two", from: "realName", to: "nickName"),
SmartExactMap(path: "two.three", from: "nick_name", to: "nickName"),
]
guard let feedThree = FeedTwo.deserialize(dict: dict, keyStrategy: .exactMap(keys2)) else { return }
您需要理解的是: 如何填写 path?
path表示您要映射的字段所在的层级。如果本身就在最顶层,path填写为 path: ""
。
在使用系统的 Codable 解码的时候,遇到 无键,值为null, 值类型错误 抛出异常导致解析失败。SmartCodable 底层默认对这三种解析错误进行了兼容。
这两种情况的数据,我称之为摆烂数据,这种数据无法拯救。
解析到 无键 & 值为null 的时候,SmartCodable会提供该字段类型的默认值进行解析填充(如果是可选类型,提供nil)。使解析顺利进行。
对这两份数据,解析到 CompatibleTypes 模型中
var json: String {
"""
{
}
"""
}
var json: String {
"""
{
"a": null,
"b": null,
"c": null,
"d": null,
"e": null,
"f": null,
"g": null,
"h": null,
"i": null,
"j": null,
"k": null,
"l": null,
"v": null,
"w": null,
"x": null,
"y": null,
"z": null
}
"""
}
struct CompatibleTypes: SmartDecodable {
var a: String = ""
var b: Bool = false
var c: Date = Date()
var d: Data = Data()
var e: Double = 0.0
var f: Float = 0.0
var g: CGFloat = 0.0
var h: Int = 0
var i: Int8 = 0
var j: Int16 = 0
var k: Int32 = 0
var l: Int64 = 0
var m: UInt = 0
var n: UInt8 = 0
var o: UInt16 = 0
var p: UInt32 = 0
var q: UInt64 = 0
var v: [String] = []
var w: [String: [String: Int]] = [:]
var x: [String: String] = [:]
var y: [String: Int] = [:]
var z: CompatibleItem = CompatibleItem()
}
class CompatibleItem: SmartDecodable {
var name: String = ""
var age: Int = 0
required init() { }
}
解析完成后,将使用该属性对应的数据类型的默认值进行填充。
guard let person = CompatibleTypes.deserialize(json: json) else { return }
/**
"属性:a 的类型是 String, 其值为 "
"属性:b 的类型是 Bool, 其值为 false"
"属性:c 的类型是 Date, 其值为 2001-01-01 00:00:00 +0000"
"属性:d 的类型是 Data, 其值为 0 bytes"
"属性:e 的类型是 Double, 其值为 0.0"
"属性:f 的类型是 Float, 其值为 0.0"
"属性:g 的类型是 CGFloat, 其值为 0.0"
"属性:h 的类型是 Int, 其值为 0"
"属性:i 的类型是 Int8, 其值为 0"
"属性:j 的类型是 Int16, 其值为 0"
"属性:k 的类型是 Int32, 其值为 0"
"属性:l 的类型是 Int64, 其值为 0"
"属性:m 的类型是 UInt, 其值为 0"
"属性:n 的类型是 UInt8, 其值为 0"
"属性:o 的类型是 UInt16, 其值为 0"
"属性:p 的类型是 UInt32, 其值为 0"
"属性:q 的类型是 UInt64, 其值为 0"
"属性:v 的类型是 Array<String>, 其值为 []"
"属性:w 的类型是 Dictionary<String, Dictionary<String, Int>>, 其值为 [:]"
"属性:x 的类型是 Dictionary<String, String>, 其值为 [:]"
"属性:y 的类型是 Dictionary<String, Int>, 其值为 [:]"
"属性:z 的类型是 CompatibleItem, 其值为 CompatibleItem(name: \"\", age: 0)"
*/
这种情况的数据,我称之为可拯救的数据。例如: Model中定义的Bool类型,数据中返回的是Int类型的0或1,String类型的True/true/Yes/No等。
解析到 值类型错误 的时候,SmartCodable会尝试对数据值进行类型转换,如果转换成功,将使用该值。如果转换失败,将使用该属性对应的数据类型的默认值进行填充。
/// 兼容Bool类型的值,Model中定义为Bool类型,但是数据中是String,Int的情况。
static func compatibleBoolType(value: Any) -> Bool? {
switch value {
case let intValue as Int:
if intValue == 1 {
return true
} else if intValue == 0 {
return false
} else {
return nil
}
case let stringValue as String:
switch stringValue {
case "1", "YES", "Yes", "yes", "TRUE", "True", "true":
return true
case "0", "NO", "No", "no", "FALSE", "False", "false":
return false
default:
return nil
}
default:
return nil
}
}
/// 兼容String类型的值
static func compatibleStringType(value: Any) -> String? {
switch value {
case let intValue as Int:
let string = String(intValue)
return string
case let floatValue as Float:
let string = String(floatValue)
return string
case let doubleValue as Double:
let string = String(doubleValue)
return string
default:
return nil
}
}
请查看 TypePatcher.swift 了解更多。
SmartCodable鼓励从根本上解决解析中的问题,即:不需要用到SmartCodable的兼容逻辑。 如果出现解析兼容的情况,修改Model中属性的定义,或要求数据方进行修正。为了更方便的定位问题,SmartCodable提供了便捷的解析错误日志。
调试日志,将提供辅助信息,帮助定位问题:
- 错误类型: 错误的类型信息
- 模型名称:发生错误的模型名出
- 数据节点:发生错误时,数据的解码路径。
- 属性信息:发生错误的字段名。
- 错误原因: 错误的具体原因。
================ [SmartLog Error] ================
错误类型: '找不到键的错误'
模型名称:Array<Class>
数据节点:Index 0 → students → Index 0
属性信息:(名称)more
错误原因: No value associated with key CodingKeys(stringValue: "more", intValue: nil) ("more").
==================================================
================ [SmartLog Error] ================
错误类型: '值类型不匹配的错误'
模型名称:DecodeErrorPrint
数据节点:a
属性信息:(类型)Bool (名称)a
错误原因: Expected to decode Bool but found a string/data instead.
==================================================
================ [SmartLog Error] ================
错误类型: '找不到值的错误'
模型名称:DecodeErrorPrint
数据节点:c
属性信息:(类型)Bool (名称)c
错误原因: c 在json中对应的值是null
==================================================
你可以通过SmartConfig 调整日志的相关设置。
右侧的数据是数组类型。注意标红的内容,由外到里对照查看。
-
Index 0: 数组的下标为0的元素。
-
sampleFive: 下标为0的元素对应的是字典,即字典key为sampleFive对应的值(是一个数组)。
-
Index 1:数组的下标为1的元素.
-
sampleOne:字典中key为sampleOne对应的值。
-
string:字典中key为sring对应的值。
struct Feed: SmartCodable {
var one: FeedOne?
}
struct FeedOne: SmartCodable {
var name: String = ""
}
如有模型中的属性是嵌套的模型属性,遇到类型不匹配的情况,Codable无法解析抛出异常,这种情况的异常,SmartCodale将无法兼容。
此时您有两种选择:
- 将Feed中one这个属性设置为非可选的。 SmartCodable 将正常工作。
- 将该属性使用 @SmartOptional 属性包装器修饰。
struct Feed: SmartCodable {
@SmartOptional var one: FeedOne?
}
class FeedOne: SmartCodable {
var name: String = ""
required init() { }
}
这是一个不得已的实现方案:
为了做解码失败的兼容,我们重写了 KeyedEncodingContainer 的 decode 和 decodeIfPresent 方法。
需要注意的是:decodeIfPresent底层的实现仍是使用的 decode。
// 系统Codable源码实现
public extension KeyedDecodingContainerProtocol {
@_inlineable // FIXME(sil-serialize-all)
public func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
guard try self.contains(key) && !self.decodeNil(forKey: key) else { return nil }
return try self.decode(Bool.self, forKey: key)
}
}
KeyedEncodingContainer容器是用结构体实现的。 重写了结构体的方法之后,没办法再调用父方法。
- 这种情况下,如果再重写
public func decodeIfPresent<T>(_ type: T.Type, forKey key: K) throws -> T?
方法,就会导致方法的循环调用。 - 使用SmartOptional属性包装器修饰可选属性,被修饰后会产生一个新的类型,对此类型解码就不会走decodeIfPresent,而是会走decode方法。
-
必须遵循SmartDecodable协议
-
必须是可选属性
如果不是可选属性,就没必要使用SmartOptional。
-
必须是class类型
如果模型是Struct,是值类型。在执行 didFinishMapping 的时候,无法初始化被属性包装器修饰的属性,进而无法有效的执行解码完成之后的值修改。
如果你有更好的方案,可以提issue。
Codable在进行解码的时候,是无法知道这个属性的。所以在decode的时候,如果解析失败,使用默认值进行填充时,拿不到这个默认值。再处理解码兼容时,只能自己生成一个对应类型的默认值填充。
如果你有更好的方案,可以提issue。
这是Swift数据解析方案的系列文章:
Swift数据解析(第四篇) - SmartCodable 上
Swift数据解析(第四篇) - SmartCodable 下
SmartCodable 是一个开源项目,我们欢迎所有对提高数据解析性能和健壮性感兴趣的开发者加入。无论是使用反馈、功能建议还是代码贡献,你的参与都将极大地推动 SmartCodable 项目的发展。