Kodable

Build Status codebeat badge codecov Platforms

Kodable 是通过属性包装器对 Codable 功能的扩展。主要目标是在添加有用功能的同时,消除样板代码。

特性

目录

安装

Swift Package Manager

如果您直接在 Package 中工作,请将 Kodable 添加到您的 Package.swift 文件中

dependencies: [
    .package(url: "https://github.com/JARMourato/Kodable.git", .upToNextMajor(from: "1.1.0")),
]

如果在 Xcode 项目中工作,请选择 File->Swift Packages->Add Package Dependency... 并搜索包名称:Kodable 或 git url

https://github.com/JARMourato/Kodable.git

用法

提供的包装器

Coding

只需让您的类型符合 Kodable,您就可以访问 Coding 带来的所有功能。您可以将 Codable 值与 Coding 属性混合搭配。

声明你的模型

struct User: Kodable {
    var identifier: String = ""
    var social: String?
    @Coding("first_name") var firstName: String
    @Coding(default: "+1 123456789") var phone: String
    @Coding("address.zipCode") var zipCode: Int
}

// Instead of

struct CodableUser: Codable {

    enum Keys: String, CodingKey {
        case identifier, social, firstName = "first_name", phone, address
    }

    enum NestedKeys: String, CodingKey {
        case zipCode
    }

    var identifier: String = ""
    var social: String?
    var firstName: String
    var phone: String
    var zipCode: Int

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        identifier = try container.decode(String.self, forKey: .identifier)
        social = try container.decodeIfPresent(String.self, forKey: .social)
        firstName = try container.decode(String.self, forKey: .firstName)
        phone = try container.decodeIfPresent(String.self, forKey: .phone) ?? "+1 123456789"
        let addressContainer = try container.nestedContainer(keyedBy: NestedKeys.self, forKey: .address)
        zipCode = try addressContainer.decode(Int.self, forKey: .zip)
    }
}

然后

let json = """
{
    identifier: "1",
    "social": 987654321,
    "first_name": John,
    "address": {
        "zipCode": 94040,
    }
}
""".data(using: .utf8)!

let result = try JSONDecoder().decode(User.self, from: json)

// or using the provided syntactic sugar

let user = try User.decode(from: json)

CodableDate

此包装器允许按属性策略解码日期。默认情况下,CodableDate 使用 iso8601 策略。内置策略有:iso8601iso8601WithMillisecondPrecisionrfc2822rfc3339timestamp。还可以通过将有效的字符串格式提供给选项 .format() 来使用自定义格式。

struct Dates: Kodable {
    @CodableDate var iso8601: Date
    @CodableDate(.format("y-MM-dd"), .key("simple_date")) var simpleDate: Date
    @CodableDate(.rfc2822, .key("rfc2822")) var rfc2822Date: Date
    @CodableDate(.rfc3339, .key("rfc3339")) var rfc3339Date: Date
    @CodableDate(.timestamp, .key("timestamp")) var timestamp: Date
}

let json = """
{
    "iso8601": "1996-12-19T16:39:57-08:00",
    "simple_date": "2001-01-01",
    "rfc2822": "Thu, 19 Dec 1996 16:39:57 GMT",
    "rfc3339": "1996-12-19T16:39:57-08:00",
    "timestamp": 978307200.0,
}
""".data(using: .utf8)!

let dates = Dates.decode(from: json)
print(dates.iso8601.description) // Prints "1996-12-20 00:39:57 +0000"
print(dates.simpleDate.description) // Prints "2001-01-01 00:00:00 +0000"
print(dates.rfc2822Date.description) // Prints "1996-12-19 16:39:57 +0000"
print(dates.rfc3339Date.description) // Prints "1996-12-20 00:39:57 +0000"
print(dates.timestamp.description) // Prints "2001-01-01 00:00:00 +0000"

请注意,目前尚不支持精度高于毫秒的 ISO8601 日期(例如,微秒或纳秒),因为 Apple 尚未正式原生支持此类精度。如果您觉得需要这些或任何其他自定义日期格式化程序,您可以实现自己的 DateConvertible 并使用 .custom(dateConvertible) DateCodingStrategy。如果您认为您的用例应该加入官方库,我们随时欢迎 PR!

高级用法

有损类型解码

对于 ArrayBoolString 类型,引入了一些有损解码。以后可以添加更多类型,但目前这些足以满足我的个人使用。要为特定属性禁用此行为,以防您希望在类型不正确时解码失败,只需将 enforceTypeDecoding 选项提供给 Coding 属性包装器。

Array

Array 上的有损解码是通过尝试以非有损方式从 Array.Element 类型解码每个元素(即使它们是 BoolString)来完成的,并且会忽略解码失败的值。

struct LossyArray: Kodable {
    @Coding("failable_array", .lossy) var array: [String]
}

let json = """
{
    "failable_array": [ "1", 1.5, "2", true, "3", null, 4 ]
}
""".data(using: .utf8)!

let lossy = try LossyArray.decode(from: json)
print(lossy.array) // Prints [ "1", "2", "3" ]

Bool

如果 Bool 失败,尝试从 IntString 解码 Bool

struct Fail: Kodable {
    @Coding("string_bool", .enforceTypeDecoding) var notBool: Bool
}

struct Success: Kodable {
    @Coding("string_bool") var stringBool: Bool
    @Coding("int_bool") var intBool: Bool
}

let json = """
{
    "string_bool": "false",
    "int_bool": 1,
}
""".data(using: .utf8)!

let success = try Success.decode(from: json)
print(success.stringBool) // prints false
print(success.intBool) // prints true

let fail = try Fail.decode(from: json) // Throws KodableError.invalidValueForPropertyWithKey("string_bool")

String

如果 String 失败,尝试从 DoubleInt 解码 String

struct Amounts: Kodable {
    @Coding("double") var double: String
    @Coding("int") var integer: String
    @Coding var string: String
}

let json = """
{
    "double": 629.9,
    "int": 1563,
    "string": "999.9"
}
""".data(using: .utf8)!

let amounts = try Amounts.decode(from: json)
print(amounts.double) // prints "629.9"
print(amounts.integer) // prints "1563"
print(amounts.string) // prints "999.9"

重写值

您可以提供带有重写闭包的 KodableModifier.custom 修饰符,以便您可以在将解码后的值分配给属性之前对其进行修改。

struct Project: Kodable {
    @Coding(.modifier(Project.trimmed)) var title: String
    
    static var trimmed: KodableModifier<String> { 
        KodableModifier { $0.trimmingCharacters(in: .whitespacesAndNewlines) } 
    }
}

let json = #"{ "title": "  A New Project    " }"#.data(using: .utf8)!

let project = try Project.decode(from: json)
print(project.title) // Prints "A New Project"

已经提供了一些内置的修饰符

String

String?

排序 当类型符合 Comparable 协议时

当类型不符合 Comparable 协议,但其一个属性符合时

如果没有完全符合 Comparable,您可以求助于基本的排序功能

Comparable

验证值

您可以提供带有验证闭包的 KodableModifier.validation 修饰符,您可以在其中验证该值是否有效。

struct Image: Kodable {
    @Coding(.validation({ $0 > 500 })) var width: Int
}

let json = #{ "width": 400 }#.data(using: .utf8)!

let image = try Image.decode(from: json)
// Throws KodableError.validationFailed(property: "width", parsedValue: 400)

自定义包装器

Kodable 基于一个名为 KodableTransform 的协议构建

public protocol KodableTransform {
    associatedtype From: Codable
    associatedtype To
    func transformFromJSON(value: From) throws -> To
    func transformToJSON(value: To) throws -> From
    init()
}

如果要添加自己的自定义行为,您可以创建一个符合 KodableTransform 协议的类型。

struct URLTransformer: KodableTransform {
    
    enum Error: Swift.Error {
        case failedToCreateURL
    }

    func transformFromJSON(value: String) throws -> URL {
        guard let url = URL(string: value) else { throw Error.failedToCreateURL }
        return url
    }
    
    func transformToJSON(value: URL) throws -> String {
        value.absoluteString
    }
}

然后使用 KodableTrasformable 属性包装器,所有其他包装器都基于此

typealias CodingURL = KodableTransformable<URLTransformer>

然后瞧!

struct Test: Kodable {
    @CodingURL("html_url") var url: URL
}

编码 Null 值

默认情况下,可选值不会被编码,因此

struct User: Kodable {
    @Coding var firstName: String
    @Coding var lastName: String?
}

let user = User()
user.firstName = "João"

编码后将输出

{
    "firstName": "João"
}

但是,如果您想显式编码 null 值,那么您可以添加 encodeAsNullIfNil 选项

struct User: Kodable {
    @Coding var firstName: String
    @Coding(.encodeAsNullIfNil) var lastName: String?
}

let user = User()
user.firstName = "João"

然后它将输出

{
    "firstName": "João",
    "lastName": null
}

调试

在开发过程中,了解接收到的 JSON 可能很有用,这样我们才能确定所选的选项会导致正确的解码。有很多方法可以做到这一点,但是,为了简单起见,Kodable 提供了一种打印接收到的 JSON 值的简单方法。

让我们以以下 JSON 和 Kodable 模型为例

{
    "identifier": "1",
    "social": 987654321,
    "first_name": "John",
    "address": {
        "zipCode": 94040,
        "state": "CA"
    },
    "aliases": [ "Jay", "Doe" ]
}

struct Address: Codable {
    let zipCode: Int
    let state: String 
}

struct User: Kodable {
    var identifier: String = ""
    var social: String?
    @Coding("first_name") var firstName: String
    @Coding(default: "+1 123456789") var phone: String
    @Coding var address: Address
}

Kodable 提供了 2 种调试将用于解码 User 模型的 JSON 的方法。第一种是检查模型的整个 JSON 值。为此,请使模型符合 DebugJSON 协议

struct User: Kodable, DebugJSON {
    /.../
}

每当解码 User 模型的实例时,您都会在控制台中收到以下消息

Decoded JSON for type User:
{
    "identifier": "1",
    "social": 987654321,
    "first_name": "John",
    "address": {
        "zipCode": 94040,
        "state": "CA"
    },
    "aliases": [ "Jay", "Doe" ]
}

但是,有时该模型可能非常广泛,您只对特定的嵌套模型感兴趣。在这种情况下,有第二个选项,即仅使用选项 .debugJSON 标记您想要的属性

struct User: Kodable {
    var identifier: String = ""
    var social: String?
    @Coding("first_name") var firstName: String
    @Coding(default: "+1 123456789") var phone: String
    @Coding(.debugJSON) var address: Address
}

在这种情况下,对于解码的 User 模型的每个实例,您都会在控制台中收到以下消息

Decoded JSON for the address property of type User:
{
    "zipCode": 94040,
    "state": "CA"
}

贡献

如果您觉得缺少某些内容或想要添加任何新功能,请打开一个 issue 请求它和/或提交一个带有通过测试的 pull 请求 🙌

许可

MIT

特别感谢

更好的解码错误消息 - 通过 @nunogoncalves

联系方式

João (@_JARMourato)