Swift:如何对一个笨拙但常规的 JSON 结构进行 DRY Decodable-parse



我们的 json-over-rest(ish( API 遵循一种编码 URL 模式,该 URL 可从特定对象以列表格式访问,在@links键下。虚构示例:

{ "id": "whatever", 
"height_at_birth": 38,
"@links": [
{ "name": "shield-activation-level",
"url": "https://example.com/some/other/path" },
{ "name": "register-genre-preference",
"url": "https://example.com/some/path" }
]
}

在 Swift 方面,我们使用 phantom 类型和可选性来实现类型安全。例如,上面的 json 可能对应于这样的结构:

struct Baby {
let id: String
let heightAtBirth: Int
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}

幻像类型确保我们不会意外地将馈送计划发布到registerGenrePreferenceURL,并且可选性指示格式正确的Baby-json 将始终包含registerGenrePreference@links条目,但其他两个链接可能存在,也可能不存在。目前为止,一切都好。

我想使用Decodable来使用这种 json 格式,理想情况下最少使用init(decoder:Decoder)个自定义实现。但我被@links条目难倒了。

我想如果我手动完成整个解码,我会是什么样子:

  1. 拿到婴儿的容器,
  2. 从中获取一个嵌套的无键容器,用于@links
  3. 遍历其值(应为[String:String]字典(并构建与 URL 匹配名称的字典
  4. 对于Baby期望的每个链接,请在字典中查找它(如果属性是非可选的并且链接丢失,则抛出(

但是步骤 2 和 3 对于遵循此模式的每个类都是相同的(不理想(,更糟糕的是,必须这样做也会阻止我使用编译器提供的Decodable实现,所以我还必须手动解码Baby所有其他属性。

如果有帮助,我非常高兴重组Baby;一个可能有帮助的明显步骤是:

struct Baby {
let id: String
let heightAtBirth: Int
let links: Links
struct Links {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
}

当然,我希望我们将不得不添加编码键,即使仅用于蛇/骆驼壳转换和@

enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}

我可能会按照上面的模式手动DecodableBaby.Links一致性,但这仍然意味着重复"获取未键入的集合,将其转换为字典,在字典中查找编码键"步骤对于遵循此模式的每个类。

有没有办法集中这个逻辑?

实际上,您的链接有一个定义良好的结构。它们是字典[String : String]因此您可以在使用Decodable时利用它来发挥自己的优势。

您可能需要考虑设置如下结构。这些链接是从 JSON 解码的,扩展程序提供了您要查找的确切链接的可选性。

可链接协议可用于将一致性添加到需要它的任何类/结构中。

import Foundation
struct Link: Decodable {
let name: String
let url: String
}
protocol Linkable {
var links: [Link] { get }
}
extension Linkable {
func url(forName name: String) -> URL? {
guard let path = links.first(where: { $0.name == name })?.url else {
return nil
}
return URL(string: path)
}
}
struct Baby: Decodable, Linkable {
let id: String
let heightAtBirth: Int
let links: [Link]
enum CodingKeys: String, CodingKey {
case id = "id"
case heightAtBirth = "height_at_birth"
case links = "@links"
}
static func makeBaby(json: String) throws -> Baby {
guard let data = json.data(using: .utf8) else {
throw CocoaError.error(.fileReadUnknown)
}
return try JSONDecoder().decode(Baby.self, from: data)
}
}
extension Baby {
var registerGenrePreference: URL? {
return url(forName: "register-genre-preference")
}
var shieldActivationLevel: URL? {
return url(forName: "shield-activation-level")
}
}
let baby = try Baby.makeBaby(json: json)
baby.registerGenrePreference
baby.shieldActivationLevel

下面的模式为我提供了完整的类型安全,并且大多数实现都存在于一次性实用程序类UntypedLinks中。每个模型类都需要定义一个嵌套类Links自定义decode(from: Decoder),但这些实现完全是样板的(可能可以通过简单的代码生成工具自动化(并且可读性合理。

public struct Baby: Decodable {
public let id: String
public let heightAtBirth: Int
public let links: Links
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}
public struct Links: Decodable {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
enum CodingKeys: String, CodingKey {
case registerGenrePreference = "register-genre-preference"
case shieldActivationLevel = "shield-activation-level"
case magicPowers = "magic-powers"
}
public init(from decoder: Decoder) throws {
let links = try UntypedLinks<CodingKeys>(from: decoder)
registerGenrePreference = try links.required(.registerGenrePreference)
shieldActivationLevel = links.optional(.shieldActivationLevel)
magicPowers = links.optional(.magicPowers)
}
}
}
public class UntypedLinks<CodingKeys> where CodingKeys: CodingKey {
let links: [String: String]
let codingPath: [CodingKey]
class UntypedLink: Codable {
let name: String
let url: String
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var links: [String: String] = [:]
while !container.isAtEnd {
let link = try container.decode(UntypedLink.self)
links[link.name] = link.url
}
self.links = links
self.codingPath = container.codingPath
}
func optional<Phantom>(_ name: CodingKeys) -> Link<Phantom>? {
return links[name.stringValue].map(Link.init)
}
func required<Phantom>(_ name: CodingKeys) throws -> Link<Phantom> {
guard let link: Link<Phantom> = optional(name) else {
throw DecodingError.keyNotFound(
name,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Link not found")
)
}
return link
}
}

相关内容

  • 没有找到相关文章

最新更新