VersionedCodable main workflow Swift 版本兼容性 Swift 平台兼容性

它是 Swift 的 Codable 的一个封装器,允许你对 Codable 类型进行版本控制,并有助于从旧版本进行增量迁移。它处理的是这样一种特殊情况:你希望能够更改类型的结构,同时保留解码其旧版本的能力。

你可以通过让你的类型遵循 VersionedCodable 来使你的类型进行版本控制。迁移以逐步的方式进行(即从 v1 到 v2 到 v3),这减少了对你的类型进行潜在的重大更改的维护负担。

这对于文档类型特别有用,在这种文档类型中,内容会定期添加、重构和移动。

Three type definitions next to each other: Poem, PoemV1, and PoemPreV1. Poem has a `static let version = 2` and has a reference to PoemV1 as its `PreviousVersion`. PoemV1's version is 1 and its PreviousVersion is PoemPreV1, whose version is nil. There's also an initializer that allows a PoemV1 to be initialized from a PoemPreV1, and a PoemV2 from a `PoemV1`.

你可以使用 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,请按照以下步骤操作

  1. 使 OldPoem 遵循 VersionedCodable。将其 version 设置为 1,将其 PreviousVersion 设置为 NothingEarlier
  2. 使 Poem 遵循 VersionedCodable。将其 version 设置为 2,将其 PreviousVersion 设置为 OldPoem
  3. Poem 定义一个接受 OldPoem 的初始化器。定义如何将旧类型转换为新类型。
  4. 在使用 JSON 版本解码 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 取代了?

并非如此。 SwiftData 是 iOS/iPadOS/tvOS 17、macOS 14、watchOS 10 和 visionOS 中新增的功能,它是对 Core Data 的一个 Swifty 接口。它确实支持模式版本控制,并且有多种方法来配置你希望数据如何持久化。它甚至可以与 DocumentGroup 一起使用。

但是,需要考虑以下几个限制

我鼓励你尝试并找到适合你的解决方案。 但我目前的建议是

是否有 Kotlin/Java/Android 的版本?

没有。 VersionedCodableUnspool 的一个开源部分,这是一个用于 macOS 的照片标记应用程序,在可预见的将来不会有 Android 版本。 我不明白为什么在 Kotlin 中做类似的事情是不可行的,但请注意 VersionedCodable 严重依赖于 Swift 具有内置的编码/解码机制和富有表现力的类型系统。 JVM 可能会使其难以以类似安全且富有表现力的方式实现相同的行为。

我们想在我们的金融/医疗/受监管的应用程序中使用它,但需要有关安全性、出处、非侵权等方面的保证。

好吧,我必须告诉你,根据 MIT 许可的条款VersionedCodable“按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于适销性、适用于特定用途和不侵权的保证”,并且“在任何情况下,作者或版权所有者均不对任何索赔、损害或其他责任负责,无论是在合同、侵权行为还是其他方面,因软件或软件的使用或其他处理方式而产生或与之相关的”。

作为一名全职软件工程师和架构师,VersionedCodable 只是我的一个副项目,我无法抽出时间来提供支持、满足采用者的监管或可追溯性要求,或者(例如)帮助你编译你的 SBOM 或 SOUP 列表。 当然,你也可以分叉它来创建一个“可信版本”,或者创建你自己的受它启发的解决方案。

仍然缺失 - 愿望清单