它是 Swift 的 Codable 的一个封装器,允许你对 Codable 类型进行版本控制,并有助于从旧版本进行增量迁移。它处理的是这样一种特殊情况:你希望能够更改类型的结构,同时保留解码其旧版本的能力。
你可以通过让你的类型遵循 VersionedCodable 来使你的类型进行版本控制。迁移以逐步的方式进行(即从 v1 到 v2 到 v3),这减少了对你的类型进行潜在的重大更改的维护负担。
这对于文档类型特别有用,在这种文档类型中,内容会定期添加、重构和移动。
你可以使用 Foundation 内置的 JSON 和属性列表编码器/解码器的扩展来编码和解码。也很容易添加对其他编码器和解码器的支持。默认情况下,版本键编码在 VersionedCodable 类型的根目录中:如果需要,你也可以指定自己的版本路径。
注意
当前的 1.2.x 系列与 Swift 5.7-5.9 存在问题。 这已在 issue #24 中跟踪。如果目前你仍然需要使用较旧的 Swift 版本,请使用 1.1.x 系列。
在一个 Swift 包中: 将这行代码添加到你的 Package.swift 文件的依赖项部分...
dependencies: [
.package(url: "https://github.com/jrothwell/VersionedCodable.git", .upToNextMinor(from: "1.1.0"))
],
或者:在 Xcode 中打开你的项目, 选择“Package Dependencies”,单击“Add”,然后输入此存储库的 URL。
阅读 VersionedCodable 的文档,可在 Web 上找到。如果你使用 Xcode,它也会出现在文档浏览器中。
一些 Codable 类型可能会随着时间的推移而发生变化,但你可能仍然需要解码旧格式的数据。 VersionedCodable 允许你保留该类型的旧版本,并使用逐步迁移的方式将其解码为当前版本。
该类型的旧版本使用其原始解码逻辑进行解码。然后将它们转换为连续的新类型,直到解码器达到目标类型。
假设你刚刚完成了对你的 Poem 类型的重构,现在看起来像这样
struct Poem: Codable {
var author: String
var poem: String
var rating: Rating
enum Rating: String, Codable {
case love, meh, hate
}
}
编码为 JSON,它看起来像这样
{
"version": 2,
"author": "Anonymous",
"poem": "An epicure dining at Crewe\nFound a rather large mouse in his stew",
"rating": "love"
}
但是,你可能仍然需要能够处理旧版本的格式的文档,这些文档看起来像这样
{
"version": 1,
"author": "Anonymous",
"poem": "An epicure dining at Crewe\nFound a rather large mouse in his stew",
"starRating": 4
}
原始类型可能看起来像这样
struct OldPoem: Codable {
var author: String
var poem: String
var starRating: Int
}
要解码和使用现有的 OldPoem JSON,请按照以下步骤操作
OldPoem 遵循 VersionedCodable。将其 version 设置为 1,将其 PreviousVersion 设置为 NothingEarlier。Poem 遵循 VersionedCodable。将其 version 设置为 2,将其 PreviousVersion 设置为 OldPoem。Poem 定义一个接受 OldPoem 的初始化器。定义如何将旧类型转换为新类型。Poem 的地方,将其更新为使用 VersionedCodable 扩展到 Foundation。你可以像这样声明遵循 VersionedCodable
extension Poem: VersionedCodable {
// Declare the current version.
// It tells us on decoding that this type is capable of decoding itself from
// an input with `"version": 3`. It also gets encoded with this version key.
static let version: Int? = 3
// The next oldest version of the `Poem` type.
typealias PreviousVersion = PoemV2
// Now we need to specify how to make a `Poem` from the previous version of the
// type. For the sake of argument, we are replacing the numeric `starRating`
// field with a "love/meh/hate" rating.
init(from oldVersion: OldPoem) {
self.author = oldVersion.author
self.poem = oldVersion.poem
switch oldVersion.starRating {
case ...2:
self.rating = .hate
case 3:
self.rating = .meh
case 4...:
self.rating = .love
default: // the field is no longer valid in the new model, so we throw an error
throw VersionedDecodingError.fieldNoLongerValid(
DecodingError.Context(
codingPath: [CodingKeys.rating],
debugDescription: "Star rating \(oldVersion.starRating) is invalid")
}
}
}
该类型的先前版本的链可以像调用堆栈允许的那样长。
如果你正在将旧类型转换为新类型,并且遇到一些数据意味着它在新数据模型中不再有意义,则可以抛出 VersionedDecodingError.fieldNoLongerValid。
对于最早版本的类型,没有更旧的版本可以尝试解码,你需要将 PreviousVersion 设置为 NothingEarlier。这是使编译器正常工作所必需的。任何尝试解码未被 VersionedCodable 链覆盖的类型的尝试都将抛出 VersionedDecodingError.unsupportedVersion(tried:)。
struct PoemV1 {
var author: String
var poem: [String]
}
extension PoemOldVersion: VersionedCodable {
static let version: Int? = 1
typealias PreviousVersion = NothingEarlier
// You don't need to provide an initializer here since you've defined `PreviousVersion` as `NothingEarlier.`
}
编写验收测试来解码类型的旧版本是一个非常好的主意。 这将使你有信心所有现有数据在当前数据模型中仍然有意义,并且你的迁移正在做正确的事情。
VersionedCodable 提供了使这些类型的迁移变得容易的基础架构,但是你仍然需要仔细考虑如何在类型的不同版本之间映射字段。类型安全不能替代测试。
提示
这种逻辑非常适合测试驱动开发,因为你已经知道成功的输入和输出是什么样的。
VersionedCodable 为 JSON 和属性列表解码器提供了围绕 Swift 默认 encode(_:) 和 decode(_:from:) 函数的薄封装。
你可以像这样解码版本化的类型
let decoder = JSONDecoder()
try decoder.decode(versioned: Poem.self, from: data) // where `data` contains your old poem
编码像这样发生
let encoder = JSONEncoder()
encoder.encode(versioned: myPoem) // where myPoem is of type `Poem` which conforms to `VersionedCodable`
这主要适用于你正在编码和解码复杂类型的情况,例如存储在某处的文档(在某人的设备的存储中、在文档数据库中等),并且不能一次性全部迁移。在这些情况下,格式通常会发生变化,并且解码逻辑通常会变得难以处理。
VersionedCodable 最初是为 Unspool 开发的,这是一个用于 MacOS 的照片标记应用程序,尚未准备好公开发布。
并非如此。 SwiftData 是 iOS/iPadOS/tvOS 17、macOS 14、watchOS 10 和 visionOS 中新增的功能,它是对 Core Data 的一个 Swifty 接口。它确实支持模式版本控制,并且有多种方法来配置你希望数据如何持久化。它甚至可以与 DocumentGroup 一起使用。
但是,需要考虑以下几个限制
@Model 类型必须是类。如果你想使用值类型,这可能不合适。SwiftData 是操作系统的一部分,不是 像 Codable 一样是 Swift 标准库的一部分。 如果你打算定位非 Apple 平台,或者早于 2023 年发布的操作系统版本,你会发现你的代码如果引用了 SwiftData 则无法编译。我鼓励你尝试并找到适合你的解决方案。 但我目前的建议是
Codable 类型进行版本控制,并且你自己处理持久化,或者你需要对值类型(struct 而不是 class)进行版本控制---请考虑 VersionedCodable。SwiftData。没有。 VersionedCodable 是 Unspool 的一个开源部分,这是一个用于 macOS 的照片标记应用程序,在可预见的将来不会有 Android 版本。 我不明白为什么在 Kotlin 中做类似的事情是不可行的,但请注意 VersionedCodable 严重依赖于 Swift 具有内置的编码/解码机制和富有表现力的类型系统。 JVM 可能会使其难以以类似安全且富有表现力的方式实现相同的行为。
好吧,我必须告诉你,根据 MIT 许可的条款,VersionedCodable“按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、适用于特定用途和不侵权的保证”,并且“在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论是在合同、侵权行为还是其他方面,因软件或软件的使用或其他处理方式而产生或与之相关的”。
作为一名全职软件工程师和架构师,VersionedCodable 只是我的一个副项目,我无法抽出时间来提供支持、满足采用者的监管或可追溯性要求,或者(例如)帮助你编译你的 SBOM 或 SOUP 列表。 当然,你也可以分叉它来创建一个“可信版本”,或者创建你自己的受它启发的解决方案。
VersionedCodable 类型中的字段在编译时发生冲突。 需要更多研究,使用当前的 Swift 编译器可能无法实现。VersionedCodable 的范围。 这也存在以下风险:向旧版本反向移植的版本将没有自动迁移路径。)