Search code examples
swiftcodablecombinedecodable

Decoding a generic decodable type


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?


Solution

  • 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()