BetterCodable 0.4.0

BetterCodable 0.4.0

Mark Sands 维护。



Better Codable through Property Wrappers

通过属性包装器提升你的 Codable 结构体。这些属性包装器的目标是避免实现自定义 init(from decoder: Decoder) throws 和忍受样板代码。

@LossyArray

@LossyArray 解码 Array 并过滤无效值,如果解码器无法解码值。这在 Array 包含非可选类型且您的 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

可选布尔类型很奇怪。曾经只表征“真或假”的类型,现在有三种可能的状态:.some(true).some(false).none。而 .none 状态可能会因为错误的决策而发生错误,误表示为“真”。

@DefaultFalse 通过默认将解码后的布尔值设为 false,以减轻这种混淆。如果解码器无法解码值,无论是遇到 null 还是某些意外的类型,都会设置默认值。

使用

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 依赖于 ISO8601DateFormatter 将字符串值解码为日期。日期编码会将值编码回原始字符串值。

使用

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 日期字符串解码为日期。日期编码会将值编码回原始字符串值。

使用

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).

时间戳策略

《时间戳策略》将 Unix 纪元时间戳的 Double 值解码为 日期。编码日期时,将值编码回原始的 时间段 值。

使用

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.

年月日策略

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

使用

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.

安装

Swift 包管理器

归属

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