前提
我有一个符合Decodable
struct
,因此它可以通过init(from:)
从各种响应中解码JSON。对于我希望解码的每种类型的 JSON 响应,我都有一个符合CodingKey
的enum
。
例
下面是一个简化的示例,可以放入 Swift 游乐场:
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)
}
与其尝试使用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 }
}
完成后映射到"人员":
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]
在Person
的 init(from:( 中没有一堆自定义的每服务(或每服务类型(功能,我认为这很难做到。不能将自定义CodingKey
符合枚举的枚举传递到decoder.container(keyedBy:)
中,因为它对该枚举的类型是通用的。
一种方法是使用自定义密钥解码策略,并从字典或通过自定义密钥解码方法/闭包中的函数执行映射。
在下面的示例中,我使用枚举来表示服务。映射字典与枚举大小写无关,因此反映了服务/服务类型键映射。希望这可以作为更复杂的实际用例的有用路线图。
import Foundation
// MARK: - Custom Key Decoding -
struct MyCodingKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
// MARK: - Services -
enum Services: String {
case service1
case service2
}
extension Services {
var mapping: [String:String] {
switch self {
case .service1: return [:]
case .service2: return ["person_name": "name"]
}
}
func getPersons(jsonData: Data) throws -> [Person] {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom { (keys: [CodingKey]) -> CodingKey in
let lastKey = keys.last!
guard lastKey.intValue == nil else {
return MyCodingKey(intValue: lastKey.intValue!)!
}
guard let stringValue = self.mapping[lastKey.stringValue] else {
return lastKey
}
return MyCodingKey(stringValue: stringValue)!
}
let persons = try decoder.decode([Person].self, from: jsonData)
return persons
}
}
// MARK: - Person Model -
struct Person: Decodable {
let name: String
}
// 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)!
// MARK: - Sample Calls -
print((try? Services.service1.getPersons(jsonData: service1JSONResponse))!)
print((try? Services.service2.getPersons(jsonData: service2JSONResponse))!)