Ios 在Swift中解码来自多个服务的JSON的更简单方法 前提

Ios 在Swift中解码来自多个服务的JSON的更简单方法 前提,ios,json,swift,decoding,decodable,Ios,Json,Swift,Decoding,Decodable,我有一个struct,它符合Decodable,因此它可以通过init(from:)从各种响应中解码JSON。对于我希望解码的每种类型的JSON响应,我都有一个符合CodingKey的enum 例子 下面是一个简化的示例,可以放在一个快速的操场上: import Foundation // MARK: - Services - struct Service1 {} struct Service2 {} // MARK: - Person Model - struct Person {

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

例子 下面是一个简化的示例,可以放在一个快速的操场上:

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
,并将其传递到
容器(keyedBy:)
,但出现以下错误:

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


如果在
Person
的init(from:)中没有一堆定制的每服务(或每服务类型)功能,我认为这将很难做到。您不能将自定义
CodingKey
-一致枚举传递到
解码器.container(keyedBy:)
,因为它是该枚举类型的泛型

一种方法是使用自定义密钥解码策略并从字典或自定义密钥解码方法/闭包中的函数执行映射

在下面的示例中,我使用了一个枚举来表示服务。映射字典被设置为与枚举大小写无关,因此反映了服务/服务类型键映射。希望这能为您更复杂的实际用例提供有用的路线图

<代码>导入基础 //标记:-自定义密钥解码- 结构MyCodingKey:CodingKey{ var stringValue:String var intValue:Int? 初始化?(stringValue:String){ self.stringValue=stringValue self.intValue=nil } 初始化?(intValue:Int){ self.stringValue=String(intValue) self.intValue=intValue } } //马克:-服务- 枚举服务:字符串{ 案例服务1 案例服务2 } 推广服务{ 变量映射:[字符串:字符串]{ 切换自身{ case.service1:返回[:] 案例服务2:返回[“人名”:“姓名”] } } func getPersons(jsonData:Data)抛出->[Person]{ let decoder=JSONDecoder() decoder.keyDecodingStrategy=.custom{(key:[CodingKey])->CodingKey in 让lastKey=keys.last! guard lastKey.intValue==nil else{ 返回MyCodingKey(intValue:lastKey.intValue!)! } guard let stringValue=self.mapping[lastKey.stringValue]else{ 返回最后一个键 } 返回MyCodingKey(stringValue:stringValue)! } let persons=尝试解码器.decode([Person].self,from:jsonData) 返回人员 } } //马克:-个人模型- 结构人:可解码{ let name:String } //马克:-回答- //来自服务1的JSON响应。 让service1JSONResponse=“” [{“姓名”:“彼得”,}] “”数据(使用:.utf8)! //来自服务2的JSON响应。 让service2JSONResponse=“” [{“人名”:“保罗”}] “”数据(使用:.utf8)! //马克:电话样本- 打印((try?Services.service1.getPersons(jsonData:service1JSONResponse))!) 打印((try?Services.service2.getPersons(jsonData:service2JSONResponse))!)
与其试图用编码键和越来越复杂的
初始化来解决这个问题,我建议通过协议来编写它:

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 }
}
然后在完成后映射到人物:

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]

将开关移到基于服务类型返回容器的私有方法。然后按正常方式解码实际上我已经尝试过了,但无法找到一个可行的实现。当我尝试将服务的正确
CodingKey.Type
传递到
container(keyedBy:)
中时,我遇到以下错误:
无法使用类型为“(keyedBy:CodingKey.Type)”的参数列表调用“container”
。你有可以分享的工作示例吗?@JWK你能展示你失败的尝试吗?@Sweeper用我的尝试更新了问题。这并不完全是你所建议的,但我认为我遇到了同样的问题。每当你发现自己在一个交换机上运行不同的代码,这对我来说意味着不同的类型。我会使用多种结构类型,或者一个类和子类。谢谢,@Rob Napier。我处理的是简单的只读对象,所以这很有意义。但是,还是坚持使用
CodingKey
?它避免了添加带有命名方案的属性,如
person\u name
。如果每种类型都指定了相应的键,那么也不必编写任何
init(from:)
实现。在我对@matt的回复中,我很快忽略了最后一点。虽然这种方法需要更多的类型,但产生的代码可以说比一个巨大的switch语句更干净,其中每种情况都复制显式解码逻辑。哦,当然,您可以使用CodingKey而不是命名属性来实现
Service2Person
。我这样做的原因是,如果在许多属性中只有少数属性被重命名,那么我相信它将需要更少的代码(codingkey必须复制每个属性名称;这种方法只在有差异时添加额外的代码)。但协议的一个关键点是实现细节
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]