首页 > 解决方案 > 在 Swift 中从多个服务中解码 JSON 的更简单方法

问题描述

前提

我有一个struct符合 的Decodable,因此它可以从各种响应中解码 JSON init(from:)。对于我希望解码的每种类型的 JSON 响应,我都有一个enum符合CodingKey.

例子

这是一个简化的示例,可以放入 Swift Playground 中:

import Foundation

// MARK: - Services -

struct Service1 {}
struct Service2 {}

// MARK: - Person Model -

struct Person {
    let name: String
}

extension Person: Decodable {
    enum CodingKeys: String, CodingKey {
        case name = "name"
    }

    enum Service2CodingKeys: String, CodingKey {
        case name = "person_name"
    }

    // And so on through service n...

    init(from decoder: Decoder) throws {
        switch decoder.userInfo[.service] {
        case is Service1.Type:
            let container = try decoder.container(keyedBy: CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
        case is Service2.Type:
            let container = try decoder.container(keyedBy: Service2CodingKeys.self)
            name = try container.decode(String.self, forKey: .name)
        // And so on through service n...
        default:
            fatalError("Missing implementation for service.")
        }
    }
}

// MARK: - CodingUserInfoKey -

extension CodingUserInfoKey {
    static let service = CodingUserInfoKey(rawValue: "service")!
}

// MARK: - Responses -

// The JSON response from service 1.
let service1JSONResponse = """
[
    {
        "name": "Peter",
    }
]
""".data(using: .utf8)!

// The JSON response from service 2.
let service2JSONResponse = """
[
    {
        "person_name": "Paul",
    }
]
""".data(using: .utf8)!

// And so on through service n... where other services have JSON responses with keys of varied names ("full_name", "personName").

// MARK: - Decoding -

let decoder = JSONDecoder()

decoder.userInfo[.service] = Service1.self
let service1Persons = try decoder.decode([Person].self, from: service1JSONResponse)

decoder.userInfo[.service] = Service2.self
let service2Persons = try decoder.decode([Person].self, from: service2JSONResponse)

问题

我遇到的问题是我有很多不同的服务需要从中解码响应,并且模型具有比这个简化示例更多的属性。随着服务数量的增加,解码这些响应所需的案例数量也会增加。

问题

如何简化init(from:)实现以减少所有这些代码重复?

尝试

我尝试CodingKey.Type为每个服务存储正确的并将其传递给container(keyedBy:),但我收到此错误:

无法使用类型为“(keyedBy:CodingKey.Type)”的参数列表调用“容器”。

init(from decoder: Decoder) throws {
    let codingKeyType: CodingKey.Type

    switch decoder.userInfo[.service] {
    case is Service1.Type: codingKeyType = CodingKeys.self
    case is Service2.Type: codingKeyType = Service2CodingKeys.self
    default: fatalError("Missing implementation for service.")
    }

    let container = try decoder.container(keyedBy: codingKeyType) // ← Error
    name = try container.decode(String.self, forKey: .name)
}

标签: iosjsonswiftdecodingdecodable

解决方案


我建议不要尝试使用 CodingKeys 和越来越复杂的 来解决这个问题init,而是建议通过协议编写它:

protocol PersonLoader: Decodable {
    var name: String { get }
    // additional properties
}

extension Person {
    init(loader: PersonLoader) {
        self.name = loader.name
        // additional properties, but this is one-time
    }
}

或者,特别是如果 Person 是一个只读的简单数据对象,您可以只使 Person 成为一个协议,然后您可以避免这个额外的复制步骤。

然后,您可以独立定义每个服务的接口:

struct Service1Person: PersonLoader {
    let name: String
}

struct Service2Person: PersonLoader {
    let person_name: String

    var name: String { person_name }
}

完成后映射到 Persons 中:

let service2Persons = try decoder.decode([Service2Person].self,
                                         from: service2JSONResponse)
    .map(Person.init)

如果您使用仅协议方法,则它看起来像这样:

protocol Person: Decodable {
    var name: String { get }
    // additional properties
}

struct Service1Person: Person {
    let name: String
}

struct Service2Person: Person {
    var name: String { person_name }
    let person_name: String
}

let service2Personsx = try decoder.decode([Service2Person].self,
                                         from: service2JSONResponse) as [Person]

推荐阅读