首页 > 解决方案 > 使用 JSONDecoder 的数组与字典响应结构

问题描述

得到以下数据模型:

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

对于以下 API 响应结构:

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

我触发 JSONDecoder 是这样的:

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

我终于尝试像这样使用解码的活动对象

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

 ...
}

现在我有两个问题:

1)

let element: T?

在此调用的 api 响应中实际上称为“活动”。但是,它可能是其他 api 响应中的合作、支付等,具有相同的 ResponseMultipleElements 周围结构。有没有办法在这里使键可交换,就像我使用泛型处理值一样?如果没有,我还能如何解决这个问题?

2)我收到此错误:

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

我告诉 Swift,detailresponse 的“campaigns”部分是一个活动对象数组 - 至少这是我在查看 api 响应时的理解。但是,错误似乎说它是一本字典。首先,我不明白为什么会这样,并且真的很想理解它。其次,我不知道如何告诉它它应该期待一个字典而不是一个数组 - 这里有点与泛型混淆。

非常感谢您提前提供的帮助!

标签: swiftgenericscodablejsondecoder

解决方案


这是一种添加自定义密钥解码策略以将任何内容映射CodingKeycount固定detailresponse值的方法element

首先创建一个自定义CodingKey

struct AnyCodingKey: CodingKey {

    var stringValue: String

    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    var intValue: Int? { return nil }

    init?(intValue: Int) {
        return nil
    }
}

然后创建类似于 Sh_Khan 的答案的结构,在大多数情况下不需要类

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

现在是有趣的部分。创建一个自定义密钥解码策略,该策略始终为不返回element的 CodingKey返回detailresponsecount

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

推荐阅读