它是 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
的范围。 这也存在以下风险:向旧版本反向移植的版本将没有自动迁移路径。)