Skip to the content.

ExCodable 1.0

ExCodable

Swift 5.10 Swift Package Manager Platforms
Build and Test GitHub Releases (latest SemVer) Deploy to CocoaPods Cocoapods
LICENSE GitHub stars @minglq

En | 中文


ExCodable 是一个 Swift 版 JSON-Model 转换工具,现在迎来重要升级,发布 1.0 版本。

「若非必要,勿造轮子」。但显然,我又造了一个,所以它一定是必要的:

ObjC 时代,最好的 JSON-Model 转换非 YYModel 莫属,可惜没有 Swift 版,所以 自己写一个吧

主要特性

struct TestExCodable: ExAutoCodable {
    @ExCodable
    var int: Int = 0
    @ExCodable("string", "s", "nested.nested.string")
    var string: String? = nil
}

上面代码虽少,但足可以体现 ExCodable 的强大。

主要特性:

使用方法

1、ExCodable

目前,使用 ExCodable 定义属性需要使用 var(可以使用 private(set) 避免属性被意外修改),并且要提供默认值。

struct TestStruct: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable("string") private(set)
    var string: String? = nil
}

2、多个候选 JSON key

struct TestAlternativeKeys: ExAutoCodable {
    @ExCodable("string", "str", "s") private(set)
    var string: String! = nil
}

3、多层嵌套的 JSON key

struct TestNestedKeys: ExAutoCodable {
    @ExCodable("nested.nested.string") private(set)
    var string: String! = nil
}

4、RawRepresentable 类型的 enum

enum TestEnum: Int, Codable {
    case zero = 0, one = 1
}

struct TestStructWithEnum: ExAutoCodable {
    @ExCodable private(set)
    var `enum` = TestEnum.zero
}

RawRepresentable 类型的 enum 需要自定义的 encode/decode。

5、自定义 Encode/Decode

@ExCodable 支持自定义 encodedecode,并且 encoderdecoder 支持使用 subscript 方式访问。

struct TestManualEncodeDecode: ExAutoCodable {
    @ExCodable(encode: { encoder, value in
        encoder["int"] = value <= 0 ? 0 : value
    }, decode: { decoder in
        if let int: Int = decoder["int"], int > 0 {
            return int
        }
        return 0
    }) private(set)
    var int: Int = 0
}

6、自动类型转换

ExCodable 支持灵活的类型转换,兼容多层嵌套的 Optional 类型,例如 Int???。并且这些转换同样适用于 RawRepresentable 类型的属性,自动转换到 RawValue 类型并调用 init(rawValue:)

A、内置支持的类型转换:

B、单个属性的自定义类型转换:

struct TestCustomEncodeDecode: ExAutoCodable {
    @ExCodable(decode: { decoder in
        if let string: String = decoder["string"] {
            return string.count
        }
        return 0
    }) private(set)
    var int: Int = 0
}

C、model 中的自定义类型转换:

struct TestCustomTypeConverter: ExAutoCodable {
    @ExCodable private(set)
    var doubleFromBool: Double? = nil
    @ExCodable private(set)
    var floatFromBool: Double? = nil
}

extension TestCustomTypeConverter: ExCodableDecodingTypeConverter {
    
    public static func decode<T: Decodable, K: CodingKey>(_ container: KeyedDecodingContainer<K>, codingKey: K, as type: T.Type) -> T? {
        
        // for nested optionals, e.g. `var int: Int??? = nil`
        let wrappedType = T?.wrappedType
        
        // decode Double from Bool
        if type is Double.Type || wrappedType is Double.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        // decode Float from Bool
        else if type is Float.Type || wrappedType is Float.Type {
            if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return (bool ? 1.0 : 0.0) as? T
            }
        }
        
        return nil
    }
}

D、全局的自定义类型转换:

struct TestCustomGlobalTypeConverter: ExAutoCodable, Equatable {
    @ExCodable private(set)
    var boolFromDouble: Bool? = nil
}

extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter {
    
    public static func decode<T: Decodable, _K: CodingKey>(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? {
        
        // for nested optionals, e.g. `var int: Int??? = nil`
        let wrappedType = T?.wrappedType
        
        // decode Bool from Double
        if type is Bool.Type || wrappedType is Bool.Type {
            if let double = try? container.decodeIfPresent(Double.self, forKey: codingKey) {
                return (double != 0) as? T
            }
        }
        
        return nil
    }
}

7、使用 Subscript 手动 Encode/Decode

对于一个未使用 ExCodable 的类型:

struct TestManualEncodingDecoding {
    let int: Int
    let string: String
}

使用 subscript 手动 encode/decode,要比 Swift 原生语法简单得多:

extension TestManualEncodingDecoding: Codable {
    
    enum Keys: CodingKey {
        case int, string
    }
    
    init(from decoder: Decoder) throws {
        int = decoder[Keys.int] ?? 0
        string = decoder[Keys.string] ?? ""
    }
    func encode(to encoder: Encoder) throws {
        encoder[Keys.int] = int
        encoder[Keys.string] = string
    }
}

8、更灵活的异常处理

ExCodable 默认忽略 JSON-Model 转换时遇到的 EncodingError.invalidValueDecodingError.keyNotFoundDecodingError.valueNotFoundDecodingError.typeMismatch 等错误,出错的属性不处理 —— encode 时跳过、decode 时保持默认值。只有 JSON 数据本身有问题时才会抛出错误。

ExCodable 也支持出错时终止转换,抛出错误:

struct TestNonnullAndThrows: ExAutoCodable {
    @ExCodable(nonnull: true, throws: true) private(set)
    var int: Int! = 0
}

9、class 以及子类

class TestClass: ExAutoCodable {
    
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable private(set)
    var string: String? = nil
    
    required init() {}
    init(int: Int, string: String?) {
        (self.int, self.string) = (int, string)
    }
}

class TestSubclass: TestClass {
    
    @ExCodable private(set)
    var bool: Bool = false
    
    required init() { super.init() }
    required init(int: Int, string: String, bool: Bool) {
        self.bool = bool
        super.init(int: int, string: string)
    }
}

10、类型推断

struct TestStruct: ExAutoCodable, Equatable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable private(set)
    var string: String? = nil
}

// 正常的转换
let json = Data(#"{"int":200,"string":"OK"}"#.utf8)
let model = try? TestStruct.decoded(from: json)

// 类型推断
let dict = try? model.encoded() as [String: Any]
let copy = try? dict.decoded() as TestStruct

更多用法参考代码中的单元测试。

安装

Swift Package Manager:

.package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0")

CocoaPods:

pod 'ExCodable', '~> 1.0.0'

升级

如果你用过 0.x 版本,感谢支持!但是时候升级了,ExCodable 依然保留了旧的 API,从而降低升级的难度。

首先,升级到 1.0 之后可以继续使用废弃的 API —— 快速、工作量小:

struct TestExCodable {
    private(set) var int: Int = 0
    private(set) var string: String?
}

extension TestExCodable: ExCodableDEPRECATED {
    static let keyMapping: [KeyMap<Self>] = [
        KeyMap(\.int, to: "int"),
        KeyMap(\.string, to: "nested.nested.string", "string", "str", "s")
    ]
    init(from decoder: Decoder) throws {
        try decode(from: decoder, with: Self.keyMapping)
    }
}

然后逐步升级到新的语法:

struct TestExCodable: ExAutoCodable {
    @ExCodable private(set)
    var int: Int = 0
    @ExCodable("nested.nested.string", "string", "str", "s") private(set)
    var string: String?
}

未来

Swift 5.9 发布时引入了 Macros,很快我就看到了基于它实现的 MetaCodable,这是目前最科学的「实现方式」。这让我一度想放弃维护 ExCodable,但我还是更喜欢 ExCodable 灵活的「使用方式」

计划未来也使用 Macros 实现重写 ExCodable,在保持目前良好特性的同时,突破 Swift 语法对目前方案的种种限制,敬请期待 —— 不确定多久 🫣

星星

如果你喜欢 ExCodable,欢迎 给个星星 ⭐️ 🤩

致敬

在此,再次,致敬 John Sundell 和 ibireme,Codextended 的非凡创意和 YYModel 的丰富特性给了我的极大的启发!

关于

我是 Míng,使用中遇到任何问题,欢迎 反馈 / i+ExCodable@iwill.im

开源

代码MIT 协议下开源。