使用 JSONDecoder 的数组与字典响应结构

Posted

技术标签:

【中文标题】使用 JSONDecoder 的数组与字典响应结构【英文标题】:Array vs Dictionary response structures with JSONDecoder 【发布时间】:2019-12-31 13:24:10 【问题描述】:

得到以下数据模型:

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

 ...

现在我有 2 个问题:

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

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

【问题讨论】:

response_type是否与campaignscooperations等不同的键相关?如果是,则将response_type 声明为枚举并根据枚举对类型进行解码。如果有固定数量的不同类型,即使是泛型也是无关紧要的。 这行不通。我的问题是:根对象中response_type 中的3 是否代表campaigns 而其他值代表其他类型? 在我最初的问题中,在 Element 类中,我有一个带有名为“element”的键的属性。该键可以是活动或合作,也可以是 api 返回的任何对象数组,取决于调用的端点。这与 response_type 无论如何都不对应。我已经开始将该对象的类型设为通用,但不知道如何对密钥进行同样的操作。有解决办法吗? 请看我的回答。 【参考方案1】:

这是一种添加自定义密钥解码策略以将detailresponse 中的任何CodingKeycount 映射到固定值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?

现在有趣的部分来了。创建一个自定义密钥解码策略,该策略始终为 detailresponse 中的 CodingKey 返回 element,而不是 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))

【讨论】:

正是我想要的。像个迷人的人一样工作,并教给我有关自定义解码策略的课程。非常感谢!!!

以上是关于使用 JSONDecoder 的数组与字典响应结构的主要内容,如果未能解决你的问题,请参考以下文章

JSONDecoder 无法处理空响应

JSONDecoder() 仅在 Swift 中处理 Null 值

使用 JSONDecoder 解码 [[String]]?

如何使用 jsonDecoder 处理来自 JSON 响应的动态键?

如何准备 API 响应以在 swift 中与 jsonDecoder 一起使用

如何在 swift 4.1 和 xcode 9.3 中使用 JSONDecoder 解码嵌套的 JSON 数组和对象?