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]
Without a bunch of custom per-service (or per-service type) functionality in Person's init(from:), I think it would be difficult to do. You can't pass custom CodingKey-conformant enums into decoder.container(keyedBy:) because it is generic over the type of that enum.
One way you could do it is by using a custom key decoding strategy and perform the mapping from a dictionary or by a function in the custom key decoding method/closure.
In the example below, I've used an enum to represent the services. The mapping dictionary is keyed off the enum case, so reflects the service/service type key mapping. Hopefully, this can serve as a useful roadmap for your more complex real-world use case.
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))!)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With