SmartCodable替换Handjson做Swift json数据解析

1. SmartCodable 导包

CocoaPods 安装

版本 安装方式 平台要求 继承功能支持
基础版 pod 'SmartCodable' iOS 12+ tvOS 12+ osx10.13+ watchOS 5.0+ visionos 1.0+ ❌ 否
继承版 pod 'SmartCodable/Inherit' iOS 13+ macOS 11+ ✅ 是

⚠️ 重要提示

  • 如果你没有强烈的继承需求,推荐使用基础版

  • 继承功能需要 Swift 宏支持Xcode 15+Swift 5.9+

2. 基础用法

遵循 SmartCodable 协议,类需要实现空初始化器:

class BasicTypes: SmartCodable {
    var int: Int = 2
    var doubleOptional: Double?
    required init() {}
}
let model = BasicTypes.deserialize(from: json)

对于结构体,编译器会提供默认的空初始化器:

struct BasicTypes: SmartCodable {
    var int: Int = 2
    var doubleOptional: Double?
}
let model = BasicTypes.deserialize(from: json)

1. 多格式输入支持

输入类型 使用示例 内部转换
字典/数组 Model.deserialize(from: dict或arr) 直接处理原生集合
JSON字符串 Model.deserialize(from: jsonString) 通过UTF-8转换为Data
二进制数据 Model.deserialize(from: data) 直接处理

2. 深度路径解析(designatedPath)

// JSON结构:
{
  "data": {
    "user": {
      "info": { ...目标内容... }
    }
  }
}

// 访问嵌套数据:
Model.deserialize(from: json, designatedPath: "data.user.info")

3. 解码策略(options)

let options: Set<SmartDecodingOption> = [
    .key(.convertFromSnakeCase),
    .date(.iso8601),
    .data(.base64)
]
策略类型 可用选项 描述
键解码 .fromSnakeCase 蛇形命名→驼峰命名
.firstLetterLower "FirstName"→"firstName"
.firstLetterUpper "firstName"→"FirstName"
日期解码 .iso8601, .secondsSince1970 完整Codable日期策略
数据解码 .base64 二进制数据处理
浮点数解码 .convertToString, .throw NaN/∞处理

⚠️ 重要: 每种策略类型只允许一个选项(重复时最后一个生效)

2.2 解码成功后调用的后处理回调

struct Model: SmartCodable {
    var name: String = ""
    mutating func didFinishMapping() {
        name = "我是 \(name)"
    }
}

2.3 键转换

定义解码时的键映射转换,优先使用第一个有效映射:

struct Model: SmartCodable {
    var name: String = ""
    var age: Int?
    
    static func mappingForKey() -> [SmartKeyTransformer]? {
        [
            CodingKeys.name <--- ["nickName", "realName"],
            CodingKeys.age <--- "stu_age",
        ]
    }
}

2.4 值转换

在JSON值和自定义类型间转换

内置值转换器

转换器 JSON类型 对象类型 描述
SmartDataTransformer String Data Base64字符串和Data对象间转换
SmartDateTransformer Any Date 处理多种日期格式(时间戳,DateFormat)转Date对象
SmartURLTransformer String URL 字符串转URL,可选编码和添加前缀
struct Model: SmartCodable {
    
    ...
    
    static func mappingForValue() -> [SmartValueTransformer]? {
        let format = DateFormatter()
        format.dateFormat = "yyyy-MM-dd"
        return [
            CodingKeys.url <--- SmartURLTransformer(prefix: "https://"),
            CodingKeys.date1 <--- SmartDateTransformer(strategy: .timestamp),
            CodingKeys.date2 <--- SmartDateTransformer(strategy: .formatted(format))
        ]
    }
}

如果需要额外解析规则,可以自己实现Transformer。遵循ValueTransformable协议实现要求:

public protocol ValueTransformable {
    associatedtype Object
    associatedtype JSON
    
    /// 从'json'转换到'object'
    func transformFromJSON(_ value: Any?) -> Object?
    
    /// 从'object'转换到'json'
    func transformToJSON(_ value: Object?) -> JSON?
}

内置快速转换器辅助

static func mappingForValue() -> [SmartValueTransformer]? {
    [
        CodingKeys.name <--- FastTransformer<String, String>(fromJSON: { json in
            "abc"
        }, toJSON: { object in
            "123"
        }),
        CodingKeys.subModel <--- FastTransformer<TestEnum, String>(fromJSON: { json in
            TestEnum.man
        }, toJSON: { object in
            object?.rawValue
        }),
    ]
}

3. 属性包装器

通过自定义属性包装器,赋予模型属性更强大的编解码行为和运行时特性,如类型兼容、键忽略、值扁平化、颜色转换和发布订阅等。这些包装器大大简化了手动处理 Codable 限制的工作,并提升了模型的表达力与灵活性。

包装器名 功能简述
@SmartAny 支持 Any 类型的编码和解码,包括 [Any][String: Any]
@IgnoredKey 忽略属性的编解码,等效于不声明在 CodingKeys 中。
@SmartFlat 将子对象的字段“扁平合并”到当前结构体的字段中进行解码/编码。
@SmartHexColor 支持将十六进制字符串自动转换为颜色对象,如 UIColor / NSColor
@SmartPublished @Published 属性自动生成支持 Codable 的 getter/setter 逻辑。

3.2 @IgnoredKey

如果需要忽略属性解析,可以重写CodingKeys或使用@IgnoredKey

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

let dict: [String: Any] = [
    "name": "Mccc"
]

let model = Model.deserialize(from: dict)
print(model)
// 输出: Model(name: "")

3.3 @SmartFlat

将结构体属性的解码/编码“扁平化处理”,即:在解析当前对象时,自动将其自身字段合并赋值给被包装的子对象

struct Model: SmartCodable {
   var name: String = ""
   var age: Int = 0
 
   @SmartFlat
   var model: FlatModel?
  
}
struct FlatModel: SmartCodable {
   var name: String = ""
   var age: Int = 0
}

let dict: [String: Any] =  [
   "name": "Mccc",
   "age": 18,
]

let model = Model.deserialize(from: dict)
print(model)
// 输出: Model(name: "Mccc", age: 18, model: FlatModel(name: "Mccc", age: 18))

3.4 @SmartHexColor

struct Model: SmartCodable {
    @SmartHexColor
    var color: UIColor?
}

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

let model = Model.deserialize(from: dict)
print(model)
// 输出: Model(color: UIExtendedSRGBColorSpace 0.490196 0.647059 0.890196 1)

3.5 @SmartPublished

class PublishedModel: ObservableObject, SmartCodable {
    required init() {}
    
    @SmartPublished
    var name: ABC?
}

struct ABC: SmartCodable {
    var a: String = ""
}

if let model = PublishedModel.deserialize(from: dict) {
    // 正确访问name属性的Publisher
    model.$name
        .sink { newName in
            print("name属性发生变化,新值为: \(newName)")
        }
        .store(in: &cancellables)
}

4. 支持继承

该功能由于使用了 Swift Macro,需要使用 Swift 5.9+,对应的 iOS 13+,因此只在SmartCodable的5.0+版本中支持。

如需要在更低版本使用继承,请查看: 低版本中的继承

如果你需要继承,请使用 @SmartSubclass 标注为子类。

4.1 基础使用

class BaseModel: SmartCodable {
    var name: String = ""
    required init() { }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
}

4.2 父类实现协议方法

class BaseModel: SmartCodable {
   var name: String = ""
   required init() { }
   
   static func mappingForKey() -> [SmartKeyTransformer]? {
       [ CodingKeys.name <--- "stu_name" ]
   }
}

@SmartSubclass
class StudentModel: BaseModel {
   var age: Int?
}

4.3 子类实现协议方法

直接实现即可,不需要 override 修饰。

class BaseModel: SmartCodable {
    var name: String = ""
    required init() { }
    
    class func mappingForKey() -> [SmartKeyTransformer]? {
        retrun nil
    }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
    
    override static func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.age <--- "stu_age" ]
    }
}

@SmartSubclass
class StudentModel: BaseModel {
var age: Int?

override static func mappingForKey() -> [SmartKeyTransformer]? {
[ CodingKeys.age <--- "stu_age" ]
}
}</pre>

4.4 父子类同时实现协议方法

需要注意几点:

  • 父类的类协议方法需要使用 class 修饰。

  • 子类的类协议方法需要获取父类的实现。

class BaseModel: SmartCodable {
    var name: String = ""
    required init() { }
    
    class func mappingForKey() -> [SmartKeyTransformer]? {
        [ CodingKeys.name <--- "stu_name" ]
    }
}

@SmartSubclass
class StudentModel: BaseModel {
    var age: Int?
    
    override static func mappingForKey() -> [SmartKeyTransformer]? {
        let trans = [ CodingKeys.age <--- "stu_age" ]
        
        if let superTrans = super.mappingForKey() {
            return trans + superTrans
        } else {
            return trans
        }
    }
}

5. 特殊支持

5.1 支持枚举

要使枚举可转换,必须遵循SmartCaseDefaultable协议:

struct Student: SmartCodable {
    var name: String = ""
    var sex: Sex = .man

    enum Sex: String, SmartCaseDefaultable {
        case man = "man"
        case woman = "woman"
    }
}
let model = Student.deserialize(from: json)

要支持 关联值枚举解码 使枚举遵循SmartAssociatedEnumerable,重写mappingForValue方法接管解码过程:

struct Model: SmartCodable {
    var sex: Sex = .man
    static func mappingForValue() -> [SmartValueTransformer]? {
        [
            CodingKeys.sex <--- RelationEnumTranformer()
        ]
    }
}

enum Sex: SmartAssociatedEnumerable {    
    case man
    case women
    case other(String)
}

struct RelationEnumTranformer: ValueTransformable {
    typealias Object = Sex
    typealias JSON = String

    func transformToJSON(_ value: Sex?) -> String? {
        // 自定义处理
    }
    func transformFromJSON(_ value: Any?) -> Sex? {
        // 自定义处理
    }
}

5.2 字符串JSON解析

SmartCodable在解码时自动处理字符串化的JSON值,无缝转换为嵌套模型对象或数组,同时保持所有键映射规则:

  • 自动解析:检测并解码字符串化JSON("{\"key\":value}")为适当对象/数组

  • 递归映射:对解析的嵌套结构应用mappingForKey()规则

  • 类型推断:根据属性类型确定解析策略(对象/数组)

struct Model: SmartCodable {
    var hobby: Hobby?
    var hobbys: [Hobby]?
}

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

let dict: [String: Any] = [
    "hobby": "{\"name\":\"sleep1\"}",
    "hobbys": "[{\"name\":\"sleep2\"}]",
]

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

5.3 兼容性

当属性解析失败时,SmartCodable会对抛出的异常进行兼容处理,确保整个解析过程不会中断:

let dict = [
    "number1": "123",
    "number2": "Mccc",
    "number3": "Mccc"
]

struct Model: SmartCodable {
    var number1: Int?
    var number2: Int?
    var number3: Int = 1
}

// 解码结果
// Model(number1: 123, number2: nil, number3: 1)

类型转换兼容性

当数据类型不匹配时(引发.typeMismatch错误),SmartCodable会尝试将String类型数据转换为所需的Int类型。

默认值填充兼容性

当类型转换失败时,会获取当前解析属性的初始化值进行填充。

5.4 更新现有模型

可适应任何数据结构,包括嵌套数组结构:

struct Model: SmartCodable {
    var name: String = ""
    var age: Int = 0
}

var dic1: [String : Any] = [
    "name": "mccc",
    "age": 10
]
let dic2: [String : Any] = [
    "age": 200
]
guard var model = Model.deserialize(from: dic1) else { return }
SmartUpdater.update(&model, from: dic2)

// 现在: model是 ["name": mccc, "age": 200].

5.5 解析超大体积数据

当解析超大体积数据时,尽量避免解析异常的兼容处理,例如:属性中声明了多个属性,且声明的属性类型不匹配。

不需要解析的属性不要使用@IgnoredKey,而是重写CodingKeys来忽略不需要解析的属性。

这样可以大幅提高解析效率。

哨兵系统(Sentinel)

SmartCodable集成了Smart Sentinel,它会监听整个解析过程。解析完成后,会显示格式化的日志信息。

这些信息仅作为辅助信息帮助您发现和纠正问题,并不意味着解析失败。

================================  [Smart Sentinel]  ================================
Array<SomeModel> 👈🏻 👀
   ╆━ Index 0
      ┆┄ a: Expected to decode 'Int' but found ‘String’ instead.
      ┆┄ b: Expected to decode 'Int' but found ’Array‘ instead.
      ┆┄ c: No value associated with key.
      ╆━ sub: SubModel
         ┆┄ sub_a: No value associated with key.
         ┆┄ sub_b: No value associated with key.
         ┆┄ sub_c: No value associated with key.
      ╆━ sub2s: [SubTwoModel]
         ╆━ Index 0
            ┆┄ sub2_a: No value associated with key.
            ┆┄ sub2_b: No value associated with key.
            ┆┄ sub2_c: No value associated with key.
         ╆━ Index 1
            ┆┄ sub2_a: Expected to decode 'Int' but found ’Array‘ instead.
   ╆━ Index 1
      ┆┄ a: No value associated with key.
      ┆┄ b: Expected to decode 'Int' but found ‘String’ instead.
      ┆┄ c: Expected to decode 'Int' but found ’Array‘ instead.
      ╆━ sub: SubModel
         ┆┄ sub_a: Expected to decode 'Int' but found ‘String’ instead.
      ╆━ sub2s: [SubTwoModel]
         ╆━ Index 0
            ┆┄ sub2_a: Expected to decode 'Int' but found ‘String’ instead.
         ╆━ Index 1
            ┆┄ sub2_a: Expected to decode 'Int' but found 'null' instead.
====================================================================================

如需使用,请开启:

SmartSentinel.debugMode = .verbose
public enum Level: Int {
    case none
    case verbose
    case alert
}

如需将日志上传到服务器:

SmartSentinel.onLogGenerated { logs in  }
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容