Bernhard Engl
Bernhard Engl

Reputation: 271

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!

Upvotes: 0

Views: 197

Answers (1)

vadian
vadian

Reputation: 285069

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))
}

Upvotes: 1

Related Questions