Swift:如何对笨拙但常规的 JSON 结构进行 DRY 可解码解析
Swift: How to DRY Decodable-parsing of an awkward but regular JSON structure
我们的 json-over-rest(ish) API 遵循编码模式 URLs 可从列表格式的特定对象访问,在 @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 方面,我们使用虚拟类型和可选性来实现类型安全。例如上面的 json 可能对应于这样的结构:
struct Baby {
let id: String
let heightAtBirth: Int
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
幽灵类型确保我们不能 post 意外地 registerGenrePreference
URL 的喂养计划,并且可选性表明格式良好的 Baby
-json 将始终包含 registerGenrePreference
的 @links
条目,但其他两个 link 可能存在也可能不存在。到目前为止一切顺利。
我想使用 Decodable
来使用这种 json 格式,最好使用最少的 init(decoder:Decoder)
自定义实现。但我被 @links
个条目难倒了。
我想我知道如果我手动完成整个解码会是什么样子:
- 拿到宝宝的容器,
- 从中获取
@links
键的嵌套无键容器
- 遍历其值(应该是
[String:String]
字典)并构建一个字典匹配名称 URLs
- 对于每个 link
Baby
期望,在字典中查找它(如果 属性 是非可选的并且 link 丢失则抛出)
但是每个 class 遵循此模式(不理想)的步骤 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]>>?
}
}
当然我希望我们必须添加编码键,即使只是为了 snake/camel-case 转换和 @
:
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}
我可能会按照上面的模式为 Baby.Links
手动 Decodable
一致性,但这仍然意味着对每个 class 重复一次 "get the unkeyed collection, transform it to a dict, look up coding-keys in the dict" 步骤遵循这种模式。
有没有办法集中这个逻辑?
您的链接实际上有一个定义明确的结构。它们是字典 [String : String]
,因此您可以利用它来使用 Decodable。
您可能需要考虑像下面这样设置您的结构。这些链接是从 JSON 解码而来的,并且扩展提供了您正在寻找的确切链接的可选性。
可链接协议可用于将一致性添加到任何需要它的class/struct。
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: { [=10=].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
以下模式为我提供了完整的类型安全,并且大部分实现都在一次性实用程序中 class UntypedLinks
。每个模型 class 都需要定义一个嵌套的 class 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
}
}
我们的 json-over-rest(ish) API 遵循编码模式 URLs 可从列表格式的特定对象访问,在 @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 方面,我们使用虚拟类型和可选性来实现类型安全。例如上面的 json 可能对应于这样的结构:
struct Baby {
let id: String
let heightAtBirth: Int
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
幽灵类型确保我们不能 post 意外地 registerGenrePreference
URL 的喂养计划,并且可选性表明格式良好的 Baby
-json 将始终包含 registerGenrePreference
的 @links
条目,但其他两个 link 可能存在也可能不存在。到目前为止一切顺利。
我想使用 Decodable
来使用这种 json 格式,最好使用最少的 init(decoder:Decoder)
自定义实现。但我被 @links
个条目难倒了。
我想我知道如果我手动完成整个解码会是什么样子:
- 拿到宝宝的容器,
- 从中获取
@links
键的嵌套无键容器 - 遍历其值(应该是
[String:String]
字典)并构建一个字典匹配名称 URLs - 对于每个 link
Baby
期望,在字典中查找它(如果 属性 是非可选的并且 link 丢失则抛出)
但是每个 class 遵循此模式(不理想)的步骤 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]>>?
}
}
当然我希望我们必须添加编码键,即使只是为了 snake/camel-case 转换和 @
:
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}
我可能会按照上面的模式为 Baby.Links
手动 Decodable
一致性,但这仍然意味着对每个 class 重复一次 "get the unkeyed collection, transform it to a dict, look up coding-keys in the dict" 步骤遵循这种模式。
有没有办法集中这个逻辑?
您的链接实际上有一个定义明确的结构。它们是字典 [String : String]
,因此您可以利用它来使用 Decodable。
您可能需要考虑像下面这样设置您的结构。这些链接是从 JSON 解码而来的,并且扩展提供了您正在寻找的确切链接的可选性。
可链接协议可用于将一致性添加到任何需要它的class/struct。
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: { [=10=].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
以下模式为我提供了完整的类型安全,并且大部分实现都在一次性实用程序中 class UntypedLinks
。每个模型 class 都需要定义一个嵌套的 class 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
}
}