AnyCodable

AnyCodable 是一个 Swift 包,它提供了处理异构或松散结构数据的工具,同时保持强大的类型安全并利用 Swift 强大的 Codable 协议。 它包括对动态编码键、解码嵌套数据以及无缝处理任何可编码值的支持。

功能特点

为什么有人需要这个?

我发现在实践中,第三方 API 可能是易变的。 当您使用私有 API 时尤其如此,这些 API 会根据使用它们的人的突发奇想而发生变化。

使用此库提供的工具可以避免为那些您不一定想实际解析或使用的代码提供刚性结构,并直接获取有用的信息,同时降低 API 崩溃的可能性。

安装

在你的 Package.swift Swift Package Manager 清单中,将以下依赖项添加到你的 dependencies 参数中

.package(url: "https://github.com/jellybeansoup/swift-any-codable.git", from: "1.0.0"),

将依赖项添加到你在清单中声明的任何目标

.target(
    name: "MyTarget",
    dependencies: [
        .product(name: "AnyCodable", package: "swift-any-codable"),
    ]
),

用法

使用 AnyCodableValue

AnyCodableValue 可以无缝地编码和解码各种原始类型。 允许将其用作占位符,当您不确定要获取哪种类型的数据时。

import AnyCodable

enum DecodingKeys: String, Hashable {
    case key, nested
}

let jsonData = Data(#"{ "key": 123, "nested": [1, 2, 3] }"#.utf8)
let decoded = try JSONDecoder().decode([DecodingKeys: AnyCodableValue].self, from: jsonData)

if let intValue = decoded[.key]?.integerValue {
    print("Decoded integer value: \(intValue)")
}

if let array = decoded[.nested]?.arrayValue {
    print("Decoded array: \(array)")
}

使用 AnyCodableKey 的灵活编码键

AnyCodableValueAnyCodableKey 的结合为处理动态或未知结构提供了灵活的解决方案,同时支持基于字符串和整数的键。 您可以以一种通过代码仍然可以访问的方式解码不熟悉的数据,同时确保可以轻松地再次对其进行编码。

import AnyCodable

struct Post: Codable {
    var title: String
    var author: String
    var unsupportedValues: [String: AnyCodableValue]

    enum CodingKeys: String, CodingKey, CaseIterable {
        case title
        case author
    }

    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.title = try container.decode(String.self, forKey: .title)
        self.author = try container.decode(String.self, forKey: .author)

        let unsupportedContainer = try decoder.container(keyedBy: AnyCodableKey.self)
        var unsupportedValues: [String: AnyCodableValue] = [:]
        for key in unsupportedContainer.allKeys where CodingKeys.allCases.map(AnyCodableKey.init).contains(key) == false {
            unsupportedValues[key.stringValue] = try unsupportedContainer.decode(AnyCodableValue.self, forKey: key)
        }
        self.unsupportedValues = unsupportedValues
    }

    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: AnyCodableKey.self)
        try container.encode(self.title, forKey: AnyCodableKey(CodingKeys.title.rawValue))
        try container.encode(self.author, forKey: AnyCodableKey(CodingKeys.author.rawValue))

        for (key, value) in self.unsupportedValues {
            try container.encode(value, forKey: AnyCodableKey(key))
        }
    }

}

let jsonData = Data(#"{"title": "Example", "author": "Jelly", "date": "2025-01-01T12:34:56Z", "draft": true}"#.utf8)
let post = try JSONDecoder().decode(Post.self, from: jsonData)
print(post) // Post(title: "Example", author: "Jelly", unsupportedValues: ["draft": .bool(true), "date": .string("2025-01-01T12:34:56Z")])

let encoded = try JSONEncoder().encode(post)
print(String(data: encoded, encoding: .utf8)!) // {"author":"Jelly","draft":true,"title":"Example","date":"2025-01-01T12:34:56Z"}

使用 InstancesOf 解码集合

InstancesOf 简化了从复杂数据中提取特定类型的过程,即使该数据嵌套在结构深处。 这极大地简化了处理 API 的过程,在这些 API 中,您只关心结构中的特定对象,或者结构本身可能会发生变化。

以这个来自非常真实的 API 的非常真实的 JSON 响应为例

{
    "data": {
        "repository": {
            "milestone": {
                "title": "v2025.1",
                "issues": {
                    "nodes": [
                         {
                             "number": 100,
                             "title": "A very real problem!"
                         },
                         {
                             "number": 101,
                             "title": "Less of a problem, more of a request."
                         },
                    ]
                }
            }
        }
    }
}

无需编写完整的结构化模型集来捕获整个响应(六个!),您可以编写两个,然后使用 InstancesOf 跳过无意义的部分,直接进入您实际关心的模型

import AnyCodable

struct Milestone: Decodable, Equatable {
    var title: String
    var issues: [Issue]

    enum CodingKeys: String, CodingKey {
        case title
        case issues
    }

    init(title: String, issues: [Issue]) {
        self.title = title
        self.issues = issues
    }

    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        issues = try container.decode(instancesOf: Issue.self, forKey: .issues)
    }

}

struct Issue: Decodable, Equatable {
    var number: Int
    var title: String
}

let milestones = try JSONDecoder().decode(InstancesOf<Milestone>.self, from: jsonData)
print(Array(milestones)) // [Milestone(title: "v2025.1", issues: [Issue(number: 100, title: "A very real problem!"), Issue(number: 101, title: "Less of a problem, more of a request.")])]

许可协议

该项目采用 BSD 2-Clause "Simplified" 许可协议。 有关详细信息,请参阅 LICENSE 文件。