最新Swift JSONToModel 库调研

1,150 阅读9分钟

1.调研库如下

以下几个在swift中应用比较多,先对几个库做个大致的了解和设定

1. Codable

Codable是苹果官方提供的一个用于简化对象编码和解码的协议,它结合了 EncodableDecodable协议,使用Codable可以轻松的与外部数据格式进行转换。

struct ZJTestCodableModel: Codable {
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    var threeDayResult: String?
    
    enum CodingKeys: String, CodingKey {
        case username, age, weight, sex, location
        case threeDayResult = "three_day_result"
    }
    //MARK: 自定义decode和encode 可自定义解析过程
     init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.username = try container.decodeIfPresent(String.self, forKey: .username)
        self.age = try container.decodeIfPresent(Int.self, forKey: .age)
        self.weight = try container.decodeIfPresent(Double.self, forKey: .weight)
        self.sex = try container.decodeIfPresent(Int.self, forKey: .sex)
        self.location = try container.decodeIfPresent(String.self, forKey: .location)
        self.threeDayResult = try container.decodeIfPresent(String.self, forKey: .threeDayResult)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(self.username, forKey: .username)
        try container.encodeIfPresent(self.age, forKey: .age)
        try container.encodeIfPresent(self.weight, forKey: .weight)
        try container.encodeIfPresent(self.sex, forKey: .sex)
        try container.encodeIfPresent(self.location, forKey: .location)
        try container.encodeIfPresent(self.threeDayResult, forKey: .threeDayResult)
    }
}

定义好模型 遵守 Codable协议,可以很方便的对数据进行encode和decode

// model转为 json
let model = ZJTestCodableModel(username: "你好",age: 18,weight: 20,sex: 1,location: "河南信阳", threeDayResult: "我就是我不一样的烟火")
if let jsonData = try? JSONEncoder().encode(model),let jsonStr = String(data:jsonData, encoding:.utf8) {
    print(jsonStr)
}

// json转为模型
let data = jsonStr.data(using: .utf8)
let model1 = try! JSONDecoder().decode(ZJTestCodableModel.self, from: data!)
  • 优点

    • 使用方便
    • 类型安全
    • 高效
    • 自定义coding过程中的编码和解码
  • 缺点

    • codingKey不支持一对多(即多个key值映射到同一个key)
    • 类型不兼容 (json中的string 不会转为定义的 int bool number等)
    • 类型不匹配直接解析失败
    • 对Any类型需要自定义encode和decode过程

2. HandyJSON(4.2k)

HandyJSON是一个用于Swift语言中的JSON序列化/反序列化库。(目前已不再更新)

与其他流行的Swift JSON库相比,HandyJSON的特点是,它支持纯swift类,使用也简单。它反序列化时(把JSON转换为Model)不要求Model从NSObject继承(因为它不是基于KVC机制),也不要求你为Model定义一个Mapping函数。只要你定义好Model类,声明它服从HandyJSON协议,HandyJSON就能自行以各个属性的属性名为Key,从JSON串中解析值。

HandyJSON目前依赖于从Swift Runtime源码中推断的内存规则,从类信息里获取所有属性的特征,包括名称,属性在内存里的偏移量、属性的个数、属性的类型等等,然后将服务端返回来的数据用操作内存的方式将数值写入对应的内存,来实现json 转model

struct ZJTestHandyJSONModel: HandyJSON {
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    var threeDayForecast: String?
    var customData: Any?
    
    mutating func mapping(mapper: HelpingMapper) {
        mapper <<<
            self.username <-- ["title","username"]
        mapper <<<
            self.threeDayForecast <-- "three_day_forecast"
    }
}

定义好相应的模型 遵守HandyJSON协议,mapping方法中返回的是需要自定义的CodingKeys

//decode
let model = ZJTestHandyJSONModel.deserialize(from: jsonStr)!
//encode
_ = model.toJSONString()!
  • 优点
    • 使用简单 方便
    • 支持CodingKey 一对多
    • 支持自定义映射过程
    • 支持数据类型兼容 (JSON中是一个int,model中是 string 会自动转化)
    • 支持动态类型(Any),能动态处理json数据中的字典键值对
  • 缺点
    • 性能问题,HandyJSON利用Mirror获取对象的metaData信息,并根据JSON数据动态设置对象的属性

    • 类型安全性不足,HandyJSON使用反射机制,类型检查发生在运行时,而不是编译时,可能有潜在的运行时错误

    • 错误信息不明细,解析失败时调试和定位问题比较困难

3. KakaJSON(1.2K)

  KakaJSON 实现原理和HandyJSON类似,通过MetaData直接访问和操作对象的内存布局,对JSON数据进行解析(目前已不再更新)

```
struct ZJKakaJSONModel: Convertible {
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    var customData: Any?
    var threeDayForecast: [ZJThreeDayKakaJSONModel]?
    var customData: Any?
    
    func kj_modelKey(from property: Property) -> ModelPropertyKey {
        switch property.name {
        case "username": return ["username", "title"]
        case "threeDayForecast": return "three_day_forecast"
        default:
            return property.name
        }
    }
}
```

KakaJSON使用简单

//decode
let data = jsonStr.kj.model(ZJKakaJSONModel.self)!
// encode
_ = data.kj.JSONString()
  • 优点

    • 使用简介方便
    • 支持CodingKey 一对多
    • 支持数据类型兼容 (json中 int model中string 会自动转化)
    • 支持解析Any类型
    • 支持自定义映射
  • 缺点

    • 性能问题
    • 类型安全性不足,由于机制问题类型检查在运行时,而不是编译器,有潜在的运行时错误
    • 错误信息不完善,问题定位比较困难
    • 更新慢,最近的版本是5年前的

4. SmartCodable(425)

SmartCodable 是一个基于Swift的Codable协议的数据解析库,旨在提供更为强大和灵活的解析能力。通过优化和重写Codable的标准功能,SmartCodable 有效地解决了传统解析过程中的常见问题,并提高了解析的容错性和灵活性。(持续更新中)

struct ZJSmartCodableModel: SmartCodable {
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    @SmartAny
    var customData: Any?
    var threeDayForecast: [ZJThreeDaySmartCodableModel] = []
    
    static func mappingForKey() -> [SmartKeyTransformer]? {
        [
            CodingKeys.threeDayForecast <--- "three_day_forecast",
            CodingKeys.username <--- ["username", "title"]
        ]
    }
}

编解码过程

  //decode
  let model = ZJSmartCodableModel.deserialize(from: jsonStr)!
  //encode
  _ = model.toJSONString() ?? ""

错误解析提示

========================  [Smart Decoding Log]  ========================
ZJSmartCodableModel 👈🏻 👀
   |- age: Expected to decode Int but found a string instead.
=========================================================================
  • 优点

    • 使用简介方便
    • 支持CodingKey 一对多
    • 支持数据类型兼容 (json中 int model中string 会自动转化)
    • 支持解析Any类型
    • 支持自定义映射
    • 异常解码日志(比较详细)
  • 缺点

    • 性能开销大
    • 第三方依赖更新问题
    • 简单模型引入该库会显的复杂

5.ExCodable

在Codable原有的功能上做了拓展,使用如下(持续更新中 更新周期慢)

//MARK: Excodable
struct ZJExcodableModel {
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    var three_day_forecast: [ZJThreeDayExcodableModel]?
}

extension ZJExcodableModel: ExCodable {
    static var keyMapping: [KeyMap<Self>] = [
        KeyMap(.username, to: "username","title"),
        KeyMap(.age, to: "age"),
        KeyMap(.weight, to: "weight"),
        KeyMap(.sex, to: "sex"),
        KeyMap(.location, to: "location"),
        KeyMap(.three_day_forecast, to: "three_day_forecast")
    ]
}

编解码操作

//decode
let model = try ZJExcodableModel.decoded(from: jsonStr.data(using: .utf8)!)
//encode  使用系统方法
let jsonData = try? encoder.encode(model)
_ = String(data: jsonData ?? Data(), encoding: .utf8) ?? ""
  • 优点

    • 支持多个CodingKey
    • 支持类型嵌套(注意类型嵌套是 key值不能做映射,否则解码失败)
    • 支持设置默认值
  • 缺点

    • 不支持数据类型自动转换,官方文档上说是支持,但是测试下来json中的字符串无法转化为model中的int、double、bool等类型
    • keyMapping中需要标记所有属性,即使codingkey和json中的相同也要全部写一遍,比较鸡肋
    • 潜在的性能开销,由于是对codable做了extension,增加了许多其他开销

6.MetaCodable

MeatCodable使用swift5.9中的新特性 macro来实现对应的功能。(持续更新中)

@Codable
struct ZJMetaCodableModel {
    //指定 title, 只支持一对一解析,
    @CodedAt("title")
    var username: String?
    var age: Int?
    var weight: Double?
    var sex: Int?
    var location: String?
    //指定使用 address中的country 隐射到 country上
    @CodedAt("address","city")
    var country: String?
    //从 address这层路径开始解析
    @CodedIn("address")
    var province:String?
    
    @CodedAt("three_day_forecast")
    var threeDayForecast: [ZJThreeDayMetaCodableModel] = []
}

编解码使用Codable提供的 decode和encode即可

  • 优点

    • 支持忽略某些属性的编解码
    • 支持解码失败设置默认值
    • 支持自定义编解码策略
    • 支持指定路径解析
    • 详细的解析错误提示
  • 缺点

    • swift版本最低要求 5.9
    • 不支持数据类型自动转换,官方文档上说是支持,但是测试下来json中的字符串无法转化为model中的int、double、bool等类型
    • 不支持Any类型解析

7. CodableWrapper

CodeableWrapper利用swift 5.9中的宏,通过属性包装来增强Codable协议的使用体验,并解决各个版本之间的不足。(持续更新中)

@Codable
struct ZJTestCodableWrapperModel {
    @CodingKey("username", "title")
    var username: String?
    var age: String?
    var weight: Double?
    var sex: Int?
    var location: String?
    
    //自定义转化的key值
    @CodingKey("three_day_forecast")
    var threeDayForecast: [ZJThreeDayCodableWrapperModel] = []
}

编解码使用Codable提供的 decode和encode即可

  • 优点

    • 支持忽略某些属性的编解码
    • 支持解码失败设置默认值
    • 支持自定义编解码策略
    • 支持指定路径解析
    • 详细的解析错误提示
    • 支持Codingkey多对一进行解析
  • 缺点

    • 不支持Any类型解析
    • 有一定的额外性能开销
    • 对简单模型使用会增加复杂性

2. 综合对比

类库名CodableHandyJSONKakaJSONSmartCodableExCodableMetaCodableCodableWrapper
字段缺失或不匹配不会导致失败
类型自适应
解析Any
属性初始化填充
自定义属性(一对多)
异常解码日志
自定义解析路径
自定义解码
安全性
接入成本easyMiddleMiddleMiddleHardMiddleMiddle
star-4.2k1.2k274130553331

3. 性能测试

对以上几个类库做性能测试,json->model,model->json对比CPU、Memory、执行速度做一个统计(测试机型 iPhone XR)。统一执行的json转model的数据如下:

fileprivate let jsonStr = """
{
    "username": "yuhanle",
    "age": 18,
    "weight": 65.4,
    "sex": 1,
    "location": "Toronto, Canada",
    "three_day_forecast": [
        {
            "conditions": "Partly cloudy",
            "day": "Monday",
            "temperature": 20
        },
        {
            "conditions": "Showers",
            "day": "Tuesday",
            "temperature": 22
        },
        {
            "conditions": "Sunny",
            "day": "Wednesday",
            "temperature": 28
        }
    ]
}
"""

各个类库性能如下表格:

4.结论

  • HandyJSONKakaJSON实现原理一致 都是通过mirror和修改metaData内存布局实现的,在内存表现上基本一致,

  • 对Codable进行拓展的几个库在内存和CPU上表现一致,重点是在支持的类型和执行时间上

    • Codable 支持类型不足 限制较多
    • ExCodable使用限制比较多,使用也很麻烦
    • MetaCodable 不支持Any类型 ,swift版本要求高
    • CodableWrapper 不支持Any类型,

SmartCodable库对于目前来说暂时是一个比较好的选择,能解决目前的几个问题:

  • 类型不匹配或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

5.参考文档

Swift Macros 元编程为Codable解码提供默认值 - 掘金

Swift JSON/Model库调研 - 掘金

有关Swift Codable解析成Dictionary 的一些事 - 掘金

Swift中的Codable