I have a struct
that conforms to Decodable
, so it can decode JSON from a variety of responses via init(from:)
. For each type of JSON response I expect to decode, I have an enum
that conforms to CodingKey
.
Here's a simplified example, which can be dropped into a 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)
The problem I'm running into is that I have a lot of different services that I needed to decode responses from, and a model with many more properties than this simplified example. As the number of services increases, so does the number of cases needed to decode those responses.
How can I simplify my init(from:)
implementation to reduce all this code duplication?
I've tried storing the correct CodingKey.Type
for each service and passing that into container(keyedBy:)
, but I get this error:
Cannot invoke 'container' with an argument list of type '(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)
}
Rather than trying to solve this with CodingKeys and an increasingly complicated init
, I suggest composing it via a protocol:
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
}
}
Alternately, particularly if Person is a read-only simple data object, you could just make Person a protocol, and then you could avoid this extra copying step.
You can then define the interfaces for each service independently:
struct Service1Person: PersonLoader {
let name: String
}
struct Service2Person: PersonLoader {
let person_name: String
var name: String { person_name }
}
And then map into Persons when you're done:
let service2Persons = try decoder.decode([Service2Person].self,
from: service2JSONResponse)
.map(Person.init)
If you went with a protocol-only approach, it would look like this instead:
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]