I need to create a generic struct that will hold any decodable type which is returned from the network, so I created something like this:
struct NetworkResponse<Wrapped: Decodable>: Decodable {
var result: Wrapped
}
so I can use the decoding method like this:
struct MyModel: Decodable {
var id: Int
var name: String
var details: String
}
func getData<R: Decodable>(url: URL) -> AnyPublisher<R, Error>
URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: NetworkResponse<R>.self, decoder: decoder)
.map(\.result)
.eraseToAnyPublisher()
//call site
let url = URL(string: "https://my/Api/Url")!
let models: [MyModel] = getData(url: url)
.sink {
//handle value here
}
But, I noticed that some responses from the network contains the result
key, and some others do not:
with result:
{
"result": { [ "id": 2, "name": "some name", "details": "some details"] }
}
without result:
[ "id": 2, "name": "some name", "details": "some details" ]
this results in the following error from the .map(\.result)
publisher because it can't find the result
key in the returned json:
(typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil)))
How can I handle either case in the NetworkResponse
struct in order to avoid such error?
The JSON your posted isn't valid, but I'm assuming it's a typo and it's actually:
{ "id": 2, "name": "some name", "details": "some details" }
// or
{ "result": { "id": 2, "name": "some name", "details": "some details" } }
({
}
instead of [
]
)
Probably the cleanest is with a manual decoder that can fall back to another type, if the first type fails:
struct NetworkResponse<Wrapped> {
let result: Wrapped
}
extension NetworkResponse: Decodable where Wrapped: Decodable {
private struct ResultResponse: Decodable {
let result: Wrapped
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let result = try container.decode(ResultResponse.self)
self.result = result.result
} catch DecodingError.keyNotFound, DecodingError.typeMismatch {
self.result = try container.decode(Wrapped.self)
}
}
}
Alternatively, you can fall back within Combine. I would not have gone with this approach, but for completeness-sake:
URLSession.shared
.dataTaskPublisher(for: url)
.map(\.data)
.flatMap { data in
Just(data)
.decode(type: NetworkResponse<R>.self, decoder: decoder)
.map(\.result)
.catch { _ in
Just(data)
.decode(type: R.self, decoder: decoder)
}
}
.eraseToAnyPublisher()