通过属性包装器改进 Codable

通过属性包装器提升您的 Codable 结构体。 这些属性包装器的目标是避免实现自定义的 init(from decoder: Decoder) throws 并摆脱样板代码的困扰。

@LossyArray

@LossyArray 解码数组,并在解码器无法解码值时过滤无效值。 当数组包含非可选类型,并且您的 API 提供的是 null 或无法在容器中解码的元素时,这非常有用。

用法

轻松过滤原始容器中的 null 值

struct Response: Codable {
    @LossyArray var values: [Int]
}

let json = #"{ "values": [1, 2, null, 4, 5, null] }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // [1, 2, 4, 5]

或者静默地排除可失败的实体

struct Failable: Codable {
    let value: String
}

struct Response: Codable {
    @LossyArray var values: [Failable]
}

let json = #"{ "values": [{"value": 4}, {"value": "fish"}] }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // [Failable(value: "fish")]

@LossyDictionary

@LossyDictionary 解码字典,并在解码器无法解码值时过滤无效的键值对。 当字典旨在包含非可选值,并且您的 API 提供的是 null 或无法在容器中解码的值时,这非常有用。

用法

轻松过滤原始容器中的 null 值

struct Response: Codable {
    @LossyDictionary var values: [String: String]
}

let json = #"{ "values": {"a": "A", "b": "B", "c": null } }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // ["a": "A", "b": "B"]

或者静默地排除可失败的实体

struct Failable: Codable {
    let value: String
}

struct Response: Codable {
    @LossyDictionary var values: [String: Failable]
}

let json = #"{ "values": {"a": {"value": "A"}, "b": {"value": 2}} }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // ["a": "A"]

@DefaultCodable

@DefaultCodable 提供了一个通用的属性包装器,允许使用自定义的 DefaultCodableStrategy 设置默认值。 这允许您为丢失的数据实现自己的默认行为,并免费获得属性包装器的行为。 以下是一些常见的默认策略,但它们也可以作为模板来实现自定义属性包装器,以满足您的特定用例。

虽然源代码中没有提供,但为您自定义的数据流创建您自己的默认策略非常容易。

struct RefreshDaily: DefaultCodableStrategy {
    static var defaultValue: CacheInterval { return CacheInterval.daily }
}

struct Cache: Codable {
    @DefaultCodable<RefreshDaily> var refreshInterval: CacheInterval
}

let json = #"{ "refreshInterval": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Cache.self, from: json)

print(result) // Cache(refreshInterval: .daily)

@DefaultFalse

可选的布尔值很奇怪。 一种曾经表示 true 或 false 的类型,现在有三种可能的状态:.some(true).some(false).none。 如果做出 BadDecisions™,.none 条件可能表示真值。

如果解码器无法解码该值(无论是遇到 null 还是遇到某种意外类型),@DefaultFalse 通过将解码后的布尔值默认为 false 来缓解这种混乱。

用法

struct UserPrivilege: Codable {
    @DefaultFalse var isAdmin: Bool
}

let json = #"{ "isAdmin": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // UserPrivilege(isAdmin: false)

@DefaultEmptyArray

可选布尔值的怪异性也扩展到其他类型,例如数组。 Soroush 有一篇很棒的博客文章解释了为什么您可能要避免使用可选数组。 不幸的是,这种想法在 Swift 中并非开箱即用。 为了将 nil 数组合并为空数组而被迫实现自定义初始化器是很痛苦的。

如果解码器无法解码容器,@DefaultEmptyArray 将解码数组并返回一个空数组,而不是 nil。

用法

struct Response: Codable {
    @DefaultEmptyArray var favorites: [Favorite]
}

let json = #"{ "favorites": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // Response(favorites: [])

@DefaultEmptyDictionary

如前所述,可选字典是 nil 和空值冲突的另一个容器。

如果解码器无法解码容器,@DefaultEmptyDictionary 将解码字典并返回一个空字典,而不是 nil。

用法

struct Response: Codable {
    @DefaultEmptyDictionary var scores: [String: Int]
}

let json = #"{ "scores": null }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // Response(values: [:])

@LosslessValue

所有功劳都归功于 Ian Keen

有时 API 可能是不可预测的。 它们可能会将某种形式的标识符或 SKU 在一个响应中视为 Int,而在另一个响应中视为 String。 或者您可能会在期望布尔值时遇到 "true"。 这就是 @LosslessValue 发挥作用的地方。

@LosslessValue 将尝试将一个值解码为您期望的类型,从而保留那些否则会引发异常或完全丢失的数据。

用法

struct Response: Codable {
    @LosslessValue var sku: String
    @LosslessValue var isAvailable: Bool
}

let json = #"{ "sku": 12345, "isAvailable": "true" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

print(result) // Response(sku: "12355", isAvailable: true)

日期包装器

Codable 的一个常见问题是解码具有混合日期格式的实体。 JSONDecoder 内置了一个方便的 dateDecodingStrategy 属性,但它对所有将要解码的日期使用相同的日期格式。 而且,通常情况下,JSONDecoder 与实体位于不同的位置,如果您选择使用它的日期解码策略,则会导致与实体紧密耦合。

属性包装器是解决上述问题的一个不错的选择。 它允许将日期格式化策略直接与实体的属性紧密绑定,并允许 JSONDecoder 与它解码的实体保持解耦。 @DateValue 包装器是跨自定义 DateValueCodableStrategy 的通用包装器。 这允许任何人实现自己的日期解码策略,并免费获得属性包装器的行为。 以下是一些常见的日期策略,但它们也可以作为模板来实现自定义属性包装器,以满足您的特定日期格式需求。

以下属性包装器深受 Ian Keen 的启发。

ISO8601Strategy

ISO8601Strategy 依赖于 ISO8601DateFormatterString 值解码为 Date。 编码日期会将该值编码为原始字符串值。

用法

struct Response: Codable {
    @DateValue<ISO8601Strategy> var date: Date
}

let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).

RFC3339Strategy

RFC3339Strategy 将 RFC 3339 日期字符串解码为 Date。 编码日期会将该值编码回原始字符串值。

用法

struct Response: Codable {
    @DateValue<RFC3339Strategy> var date: Date
}

let json = #"{ "date": "1996-12-19T16:39:57-08:00" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

// This produces a valid `Date` representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC (Pacific Standard Time).

TimestampStrategy

TimestampStrategy 将 unix epoch 的 Double 解码为 Date。 编码日期会将该值编码为原始 TimeInterval 值。

用法

struct Response: Codable {
    @DateValue<TimestampStrategy> var date: Date
}

let json = #"{ "date": 978307200.0 }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

// This produces a valid `Date` representing January 1st, 2001.

YearMonthDayStrategy

@DateValue<YearMonthDayStrategy> 使用日期格式 y-MM-dd 将字符串值解码为 Date。 编码日期会将该值编码回原始字符串格式。

用法

struct Response: Codable {
    @DateValue<YearMonthDayStrategy> var date: Date
}

let json = #"{ "date": "2001-01-01" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

// This produces a valid `Date` representing January 1st, 2001.

或者,最后,您可以根据需要混合和匹配日期包装器,这才是真正发挥作用的地方

struct Response: Codable {
    @DateValue<ISO8601Strategy> var updatedAt: Date
    @DateValue<YearMonthDayStrategy> var birthday: Date
}

let json = #"{ "updatedAt": "2019-10-19T16:14:32-05:00", "birthday": "1984-01-22" }"#.data(using: .utf8)!
let result = try JSONDecoder().decode(Response.self, from: json)

// This produces two valid `Date` values, `updatedAt` representing October 19, 2019 and `birthday` January 22nd, 1984.

安装

CocoaPods

pod 'BetterCodable', '~> 0.1.0'

Swift Package Manager

归属

本项目在 MIT 许可下发布。 如果您觉得这些有用,请告诉您的老板您在哪里找到它们的。