SmartCodable

3.4.3

SmartCodable is a data parsing library based on Codable. It is simple to use, with robust compatibility being one of its main features. SmartCodable 是基于Codable实现的数据解析库。简单易用,强悍的兼容性是SmartCodable的主要特点。 表层API和功能几乎和HandyJSON一致,支持快速的迁移。
intsig171/SmartCodable

What's New

SmartAny使用优化

2024-05-15T12:32:44Z

包含SmartAny的model转json失败的优化

✨✨✨看起来还不错?给个star✨吧,急需支持✨✨✨

English ReadMe

🌐 If you need,please visit >>> English README

SmartCodable - Swift数据解析

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

HandyJSON vs Codable

【✅: 完美支持】【⚠️: 带缺陷的支持】【❌: 不支持】

序号 🎯 特性 💬 特性说明 💬 SmartCodable HandyJSON
1 强大的兼容性 完美兼容:字段缺失 & 字段值为nul & 字段类型错误
2 类型自适应 如JSON中是一个Int,但对应Model是String字段,会自动完成转化
3 解析Any 支持解析 [Any], [String: Any] 等类型
4 解码回调 支持Model解码完成的回调,即:didFinishingMapping
5 属性初始化值填充 当解析失败时,支持使用初始的Model属性的赋值。
6 字符串的Model化 字符串是json字符串,支持进行Model化解析
7 枚举的解析 当枚举解析失败时,支持兼容。
8 属性的自定义解析 - 重命名 自定义解码key(对解码的Model属性重命名)
9 属性的自定义解析 - 忽略 忽略某个Model属性的解码
10 支持designatedPath 实现自定义解析路径
11 Model的继承 在model的继承关系下,Codable的支持力度较弱,使用不便(可以支持)
12 自定义解析路径 指定从json的层级开始解析
13 超复杂的数据解码 解码过程中,多数据做进一步的整合/处理。如: 数据的扁平化处理 ⚠️
14 解码性能 在解码性能上,SmartCodable 平均强 30% ⚠️
15 异常解码日志 当解码异常进行了兼容处理时,提供排查日志
16 安全性方面 底层实现的稳定性和安全性。
17 支持designatedPath 实现自定义解析路径

整体来讲: SmartCodable 和 HandyJSON 相比,在功能和使用上相近。

安全性 & 稳定性

  • HandyJSON 使用Swift的反射特性来实现数据的序列化和反序列化。该机制是非法的,不安全的, 更多的细节请访问 HandyJSON 的466号issue.

  • Codable 是Swift标准库的一部分,提供了一种声明式的方式来进行序列化和反序列化,它更为通用。

建议 & 回答

有不少使用者提出了优化需求 或 新功能的要求。在这边逐一回复:

💡 建议列表 是否采纳 理由
#suggest 1 在mapping方法中支持解析忽略 不采纳的理由
#suggest 2 像HandyJSON一样支持继承关系的解析 不采纳的理由
#suggest 3 支持初始值填充 实现逻辑
#suggest 4 提供HandyJSON的替换指导 替换指导
#suggest 5 提供全局的Key映射策略 实现逻辑
#suggest 6 支持UIColor的解析 实现逻辑
#suggest 7 增加单个Value的自定义转换策略 实现逻辑
#suggest 8 支持designatedPath的自定义路径解析 参考HandyJSON实现

使用SmartCodable 平替 HandyJSON

更多内容请查看: 替换指导

内容项 内容项说明 使用场景 替换难度 评判理由
①声明Model 声明Model ★★★★★ ★☆☆☆☆ 全局将 HandyJSON 替换为 SmartCodable即可。
②反序列化 数据的模型化(数据转Model) ★★★★★ ☆☆☆☆☆ 完全一样的调用方式,无需处理。
③序列化 模型的数据化(Model转数据) ★☆☆☆☆ ★☆☆☆☆ toJSON() 替换为 toDictionary()toArray()
④解码完成的回调 解析完成进一步处理数据 ★★☆☆☆ ☆☆☆☆☆ 完全一样的调用方式,无需处理。
⑤自定义解析Key 忽略key的解析 & 自定义Key的映射关系 ★★★☆☆ ★★★☆☆ 需要更改调用方式。
⑥解析Any 解析Any类型的数据。Any,[String: Any], [Any] ★☆☆☆☆ ★☆☆☆☆ 将Any替换为SmartAny
⑦处理继承关系 解析存在的继承关系的Model ★☆☆☆☆ ★★★★★ 建议使用协议实现。
⑧枚举的解析 解析枚举属性 ★☆☆☆☆ ★☆☆☆☆ 多实现一个 defaultCase

集成 SmartCodable

cocopods 集成

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'
use_frameworks!

target 'MyApp' do
  pod 'SmartCodable'
end

SPM 集成

https://github.com/intsig171/SmartCodable.git

SmartCodable 使用介绍

1. 字典的解码

import SmartCodable

struct Model: SmartCodable {
    var name: String = ""
}

let dict: [String: String] = ["name": "xiaoming"]
guard let model = Model.deserialize(from: dict) else { return }

2. 数组的解码

import SmartCodable

struct Model: SmartCodable {
    var name: String = ""
}

let dict: [String: String] = ["name": "xiaoming"]
let arr = [dict, dict]
guard let models = [Model].deserialize(from: arr) else { return }

3. 序列化与反序列化

// 字典转模型
guard let xiaoMing = JsonToModel.deserialize(from: dict) else { return }

// 模型转字典
let studentDict = xiaoMing.toDictionary() ?? [:]

// 模型转json字符串
let json1 = xiaoMing.toJSONString(prettyPrint: true) ?? ""

// json字符串转模型
guard let xiaoMing2 = JsonToModel.deserialize(from: json1) else { return }

4. 解码Any

Codable是无法解码Any类型的,这样就意味着模型的属性类型不可以是 Any[Any][String: Any]等类型, 这对解码造成了一定的困扰。SmartAny 是SmartCodable 提供的解决Any的方案。可以直接像使用 Any 一样使用它。

struct AnyModel: SmartCodable {
    var name: SmartAny?
    var dict: [String: SmartAny] = [:]
    var arr: [SmartAny] = []
}
let dict = [
    "name": "xiao ming",
    "age": 20,
    "dict": inDict,
    "arr": arr
] as [String : Any]

guard let model = AnyModel.deserialize(from: dict) else { return }
print(model.name.peel )
print(model.dict.peel)
print(model.arr.peel)

真实的数据被 SmartAny 包裹住了,需要使用 peel 对数据解包。

编码成SmartAny

同时也提供了反向转换的方法:

From To Example
Any SmartAny SmartAny(from: "some")
[String: Any] [String: SmartAny] ["key2": "value2"].cover
[Any] [SmartAny] [ ["key3": "value3"] ].cover

5. 模型化解析json字符串

let dict: [String: Any] = [
    "hobby": "{\"name\":\"sleep\"}",
]
guard let model = Model.deserialize(from: dict) else { return }
print(model)

struct Model: SmartCodable {
    var hobby: Hobby?
}

struct Hobby: SmartCodable {
    var name: String = ""
}

6. 支持解析UIColor

let dict = [
    "color": "7DA5E3"
]

struct Model: SmartCodable {
    var color: SmartColor?
}

guard let model = Model.deserialize(from: dict) else { return }
print(model.color?.peel)

UIColornon-final class。非最终类不能简单地实现Codableinit(from:)。具体可查阅 suggest 6

7. 枚举的解码

让枚举遵循 SmartCaseDefaultable ,当解码失败时使用 defaultCase

struct CompatibleEnum: SmartCodable {

    var enumTest: TestEnum?

    enum TestEnum: String, SmartCaseDefaultable {
        case a
        case b
        case c = "hello"
    }
}

支持关联值枚举的解码

完全交给你自定义解析规则。 如果不自定义,不进行解析。

/// 关联值枚举的解析, 需要自己接管decode
enum Sex: SmartAssociatedEnumerable {
    static var defaultCase: Sex = .women
    
    case man
    case women
    case other(String)
}
struct CompatibleEnum: SmartCodable {
    var sex: Sex = .man
    static func mappingForValue() -> [SmartValueTransformer]? {
        [
            CodingKeys.sex <--- RelationEnumTranformer()
        ]
    }
}

struct RelationEnumTranformer: ValueTransformable {
    func transformToJSON(_ value: Introduce_8ViewController.Sex?) -> String? {
        guard let value = value else { return nil }
        
        switch value {
        case .man:
            return "man"
        case .women:
            return "women"
        case .other(let v):
            return v
        }
    }

    typealias Object = Sex
    typealias JSON = String
    
    func transformFromJSON(_ value: Any?) -> Sex? {
        guard let temp = value as? String else { return .man }

        switch temp {
        case "man":
            return .man
        case "women":
            return .women
        default:
            return .other(temp)
        }
    }
}

SmartCodable的解码策略

解码策略分为三个阶段的操作:

  • 解码前
    • 忽略某些key的解析
  • 解码中
    • Key的映射策略
    • Value的解析策略
  • 解码后
    • 解码完成后的回调

[解码前] 忽略某些key的解析

struct Model: SmartCodable {
    var name: String = ""
    var ignore: String = ""
    var age: Int = 0
    
    enum CodingKeys: String, CodingKey {
        case name
        case age = "selfAge"
        // 忽略ignore的解析。
//            case ignore
    }
}

重写当前的 CodingKeys,不需要解析谁,就删掉谁。留下的是需要解析的。

当然,也可以在这里进行key的重命名。

@IgnoredKey

也可以选择使用 @IgnoredKey 属性包装器忽略某个Key的解析。

struct Home: SmartCodable {
    var name: String = ""
    @IgnoredKey
    var age: [Any] = ["1"]
    @IgnoredKey
    var area: String = "area"
}

严格意义上来说: 要忽略解析只能通过重写 CodingKeys。 作者通过**@IgnoredKey** 的标记打断了解析过程(还是会进入解析 ),返回了该属性的初始化值。

[解码中] Key的映射策略

全局的Key映射策略

public enum SmartKeyDecodingStrategy : Sendable {
    case useDefaultKeys
    
    // 蛇形命名转驼峰命名
    case fromSnakeCase
    
    // 首字母大写转小写
    case firstLetterLower
}

作用于本次解析,本次解析只能使用一种策略,不可混合使用。

let option1: SmartDecodingOption = .key(.fromSnakeCase)
guard let model1 = TwoModel.deserialize(from: dict1, options: [option1]) else { return }

局部的Key映射策略

  • 支持自定义路径解析。
  • 支持字段解析的重命名。
自定义解析路径

跨层解析。将sub里面的name字段,解析到 Model的name属性上。

let dict = [
    "age": 10,
    "sub": [
        "name": "Mccc"
    ]
]
struct Model: SmartCodable {
    var age: Int = 0
    var name: String = ""
    static func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.name <--- "sub.name" ]
    }
}
重命名key

支持自定义映射关系,你需要实现一个可选的mapping函数。

struct Model: SmartCodable {
    var name: String = ""
    var age: Int = 0
    var ignoreKey: String?
    
    enum CodingKeys: CodingKey {
        case name
        case age
    }
    
    static func mappingForKey() -> [SmartKeyTransformer]? {
        [
            CodingKeys.name <--- ["nickName", "realName"],
            CodingKeys.age <--- "person_age"
        ]
    }
}
  • 1对1 的映射

    你可以选择像 CodingKeys.age <--- "person_age" ,只处理1对1的映射。

  • 1对多 的映射

    也可以像 CodingKeys.name <--- ["nickName", "realName"] 一样处理 1对多 的映射。如果恰好都有值,将选择第一个。

[解码中] Value的解析策略

全局的值解析策略

SmartDecodingOption提供了三种解码选项,分别为:

public enum SmartDecodingOption {
    
    /// 用于解码 “Date” 值的策略
    case dateStrategy(JSONDecoder.DateDecodingStrategy)
    
    /// 用于解码 “Data” 值的策略
    case dataStrategy(JSONDecoder.DataDecodingStrategy)
    
    /// 用于不符合json的浮点值(IEEE 754无穷大和NaN)的策略
    case floatStrategy(JSONDecoder.NonConformingFloatDecodingStrategy)
}
Date
let dateFormatter = DateFormatter()
 dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let option: JSONDecoder.SmartDecodingOption = .dateStrategy(.formatted(dateFormatter))
guard let model = FeedOne.deserialize(from: json, options: [option]) else { return }
Data
let option: JSONDecoder.SmartDecodingOption = .dataStrategy(.base64)
guard let model = FeedOne.deserialize(from: json, options: [option]) else { return }
gurad let data = model.address, let url = String(data: data, encoding: .utf8) { else }
Float
let option: JSONDecoder.SmartDecodingOption = .floatStrategy(.convertFromString(positiveInfinity: "infinity", negativeInfinity: "-infinity", nan: "NaN"))
guard let model1 = FeedOne.deserialize(from: json, options: [option]) else {  return }

局部的值解析策略

struct SmartModel: SmartCodable {
    var date1: Date?
    var date2: Date?
    var url: URL?
    var data: Data?
            
    // value的解析策略
    static func mappingForValue() -> [SmartValueTransformer]? {
        let format = DateFormatter()
        format.dateFormat = "yyyy-MM-dd"
        return [
            CodingKeys.url <--- SmartURLTransformer(prefix: "https://"),
            CodingKeys.date2 <--- SmartDateTransformer(),
            CodingKeys.date1 <--- SmartDateFormatTransformer(format),
            CodingKeys.data <--- SmartDataTransformer()
        ]
    }
}

您可以实现 mappingForValue 给每个属性设置不同的解析策略。

支持的类型:

  • Date
  • UIColor
  • URL
  • Data

如需其他类型可以提 issue

自定义解析策略

遵守该协议,实现协议方法。

public protocol ValueTransformable {
    associatedtype Object
    associatedtype JSON
    
    /// transform from ’json‘ to ’object‘
    func transformFromJSON(_ value: Any?) -> Object?
    
    /// transform to ‘json’ from ‘object’
    func transformToJSON(_ value: Object?) -> JSON?
}

[解码后] 解析完成的回调

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
        }
    }
}

调试日志

出现 SmartLog Error 日志代表着 SmartCodable 遇到了解析问题,走进了兼容。 并不代表着本次解析失败。

SmartCodable鼓励从根本上解决解析中的问题,即:不需要用到SmartCodable的兼容逻辑。 如果出现解析兼容的情况,修改Model中属性的定义,或要求数据方进行修正。为了更方便的定位问题。

调试日志,将提供辅助信息

 ========================  [Smart Decoding Log]  ========================
 Family 👈🏻 👀
    |- name    : Expected to decode String but found an array instead.
    |- location: Expected to decode String but found an array instead.
    |- date    : Expected to decode Date but found an array instead.
    |> father: Father
       |- name: Expected String value but found null instead.
       |- age : Expected to decode Int but found a string/data instead.
       |> dog: Dog
          |- hobby: Expected to decode String but found a number instead.
    |> sons: [Son]
       |- [Index 0] hobby: Expected to decode String but found a number instead.
       |- [Index 0] age  : Expected to decode Int but found a string/data instead.
       |- [Index 1] age  : Expected to decode Int but found an array instead.
 =========================================================================

新版本的日志:

  • 以Model为单元进行和合并,减少了日志的数量。

  • 合并相同的解析错误,减少了相同日志的干扰。

  • 简化了日志内容,让日志一目了然。

进一步了解

我们提供了详细的示例工程,可以下载工程代码查看。

了解更多关于Codable & SmartCodable

这是Swift数据解析方案的系列文章:

Swift数据解析(第一篇) - 技术选型

Swift数据解析(第二篇) - Codable 上

Swift数据解析(第二篇) - Codable 下

Swift数据解析(第三篇) - Codable源码学习

Swift数据解析(第四篇) - SmartCodable 上

Swift数据解析(第四篇) - SmartCodable 下

联系我们

QQ

加入我们

SmartCodable 是一个开源项目,我们欢迎所有对提高数据解析性能和健壮性感兴趣的开发者加入。无论是使用反馈、功能建议还是代码贡献,你的参与都将极大地推动 SmartCodable 项目的发展。

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

  • None
Last updated: Tue May 28 2024 04:31:03 GMT-0900 (Hawaii-Aleutian Daylight Time)