Search code examples
swiftgenericscodablejsondecoder

Array vs Dictionary response structures with JSONDecoder


Got the following data model:

class ResponseMultipleElements<Element: Decodable>: Decodable {
    let statuscode: Int
    let response_type: Int
    let errormessage: String?
    let detailresponse: Element?

}

class Element<T: Decodable>: Decodable {
    let count: String;
    let element: T?
}

For the following API response structure:

{
    "statuscode": 200,
    "response_type": 3,
    "errormessage": null,
    "detailresponse": {
        "count": "1",
        "campaigns": [
            {
                "id": 1,
                "name": "Foo",
                "targetagegroup": null,
                "creator":...
                ...
            }
      }
}

I'm triggering JSONDecoder like this:

class APIService: NSObject {   

func getCampaignList(completion: @escaping(Result<[Campaign], APIError>) -> Void) {

            guard let endpoint = URL(string: apiBaseUrlSecure + "/campaignlist") else {fatalError()}
            var request = URLRequest(url: endpoint)
            request.addValue("Bearer " + UserDefaults.standard.string(forKey: "authtoken")!, forHTTPHeaderField: "Authorization")
            request.httpMethod = "GET"

            let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200, let jsonData = data
                    else { print("ERROR: ", error ?? "unknown error"); completion(.failure(.responseError)); return }
                do {
                    let response = try JSONDecoder().decode(ResponseMultipleElements<[Campaign]>.self, from: jsonData)
                    completion(.success(response.detailresponse!))

                } catch {
                    print("Error is: ", error)
                    completion(.failure(.decodingError))
                }
            }
            dataTask.resume()
        }
 ...
}

And I'm finally trying to make use of the decoded campaign object like this

class CoopOverviewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {

override func viewDidLoad() {
        super.viewDidLoad()
        //do stuff

        // load Campaigns
        self.apiService.getCampaignList(completion: {result in
            switch result {
            case .success(let campaigns):
                DispatchQueue.main.async {
                    print("CAMPAIGN DATA: ", campaigns[0].name)
                }
            case .failure(let error):
                print("An error occured \(error.localizedDescription)")
            }
        })

 ...
}

Now I've got 2 questions:

1)

let element: T?

is actually called "campaigns" in the api response for this call. However, it could be cooperations, payments, etc. in other api responses with that same ResponseMultipleElements surrounding structure. Is there a way to make the key swappable here, like I've done with the value with the use of generics? If not, how else would I solve that problem?

2) I'm getting this error:

typeMismatch(Swift.Array<Any>, 
Swift.DecodingError.Context(codingPath: 
[CodingKeys(stringValue: "detailresponse", intValue: nil)], 
debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

I've told Swift that the "campaigns" part of the detailresponse is an Array of campaign objects - at least that's my understanding when looking at the api response. However, the error seems to say it's a dictionary. First, I don't get why that is and would really like to understand it. Second, I don't know how to tell it that it should expect a dictionary instead of an array then - getting confused with generics here a bit.

Thank you so much for your help in advance!


Solution

  • This is an approach to add a custom key decoding strategy to map any CodingKey but count in detailresponse to fixed value element.

    First of all create a custom CodingKey

    struct AnyCodingKey: CodingKey {
    
        var stringValue: String
    
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
    
        var intValue: Int? { return nil }
    
        init?(intValue: Int) {
            return nil
        }
    }
    

    Then create the structs similar to Sh_Khan's answer, in most cases classes are not needed

    struct ResponseMultipleElements<T: Decodable>: Decodable {
        let statuscode : Int
        let response_type : Int
        let errormessage : String?
        let detailresponse : Element<T>
    }
    
    struct Element<U: Decodable>: Decodable {
        let count : String
        let element : U
    }
    
    struct Campaign : Decodable {
        let id : Int
        let name : String
        let targetagegroup : String?
    }
    

    Now comes the funny part. Create a custom key decoding strategy which returns always element for the CodingKey in detailresponse which is not count

    do {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .custom { codingKeys in
            let lastKey = codingKeys.last!
            if lastKey.intValue != nil || codingKeys.count != 2 { return lastKey }
            if lastKey.stringValue == "count" { return lastKey }
            return AnyCodingKey(stringValue: "element")!
        }
        let result = try decoder.decode(ResponseMultipleElements<[Campaign]>.self, from: data)
        completion(.success(result.detailresponse.element))
    } catch {
        print("Error is: ", error)
        completion(.failure(error))
    }