解码 JSON 编码不佳(嵌套过多,许多可能的键,键与值混淆)

Decoding JSON with poor encoding (excessive nesting, many possible keys, keys confused with values)

我正在从 API 中获取数据,如下所示:

enum MyError : Error {
    case FoundNil(String)
}

struct Crypto : Decodable {
    private enum CodingKeys : String, CodingKey { case raw = "RAW" }
    let raw : CryptoRAW
}

struct CryptoRAW : Decodable {
    private enum CodingKeys : String, CodingKey {
        case btc = "BTC"
        case xrp = "XRP"
    }
    let btc : CryptoCURRENCIES?
    let xrp : CryptoCURRENCIES?
}

struct CryptoCURRENCIES : Decodable {
    private enum CodingKeys : String, CodingKey {
        case usd = "USD"
        case eur = "EUR"
    }
    let usd : CryptoCURRENCY?
    let eur : CryptoCURRENCY?
}

struct CryptoCURRENCY : Decodable {
    let price : Double
    let percentChange24h : Double

    private enum CodingKeys : String, CodingKey {
        case price = "PRICE"
        case percentChange24h = "CHANGEPCT24HOUR"
    }
}

class CryptoInfo : NSObject {

    enum FetchError: Error {
        case urlError
        case unknownNetworkError
    }

    func fetchCryptoInfo(forCrypto crypto: String, forCurrency currency: String, _ completion: @escaping (Crypto?, Error?) -> Void) {
        let url = URL(string: "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=\(crypto)&tsyms=\(currency)")!
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data, error == nil else {
                completion(nil, error ?? FetchError.unknownNetworkError)
                return
            }
            do {
                let crypto = try JSONDecoder().decode(Crypto.self, from: data); completion(crypto, nil)
            } catch let parseError {
                completion(nil, parseError)
            }
        }
        task.resume()
    }
}

如您所见,我可以通过相同的结构获得不同的东西:btcxrp in usdeur 取决于调用中传递的参数功能(不包括在内以尽量保持代码尽可能短)。

当我想访问 Double 值时 API returns 这里是我所做的:

if let price = Crypto.raw.btc?.usd?.price { MainViewController.bitcoinDoublePrice = price }

一切都很好,但我在这里有一个很大的优化问题:这是我需要做的才能在 usdeur 中同时获得 btcxrp :

if let price = Crypto.raw.btc?.usd?.price { MainViewController.bitcoinUSDDoublePrice = price }
if let price = Crypto.raw.btc?.eur?.price { MainViewController.bitcoinEURDoublePrice = price }
if let price = Crypto.raw.xrp?.usd?.price { MainViewController.rippleUSDDoublePrice = price }
if let price = Crypto.raw.xrp?.eur?.price { MainViewController.rippleEURDoublePrice = price }

如果我只有这四个就好了,但我需要获取 5 种不同货币的 25 种不同的加密货币 + 它们在不同时间范围内的百分比变化。

我想你现在明白了。

我可以做些什么来通过向函数传递参数来动态替换 let price = cryptoInfo.raw.btc?.usd?.price 中的 btclet price = cryptoInfo.raw.xrp?.eur?.price 中的 eur,或者可能有不同的方法来避免我想不到的重复?

示例JSON 输入:

"RAW":{  
  "BTC":{  
     "USD":{  
        "TYPE":"5",
        "MARKET":"CCCAGG",
        "FROMSYMBOL":"BTC",
        "TOSYMBOL":"USD",
        "FLAGS":"4",
        "PRICE":10248.64,
        "LASTUPDATE":1519669598,
        "LASTVOLUME":0.14558,
        "LASTVOLUMETO":1489.13782,
        "LASTTRADEID":"203305344",
        "VOLUMEDAY":92548.48622803023,
        "VOLUMEDAYTO":924032126.7547476,
        "VOLUME24HOUR":107957.56694427232,
        "VOLUME24HOURTO":1072399848.5990984,
        "OPENDAY":9610.11,
        "HIGHDAY":10409.28,
        "LOWDAY":9411.82,
        "OPEN24HOUR":9466.87,
        "HIGH24HOUR":10414.1,
        "LOW24HOUR":9396.22,
        "LASTMARKET":"Bitfinex",
        "CHANGE24HOUR":781.7699999999986,
        "CHANGEPCT24HOUR":8.257956431217483,
        "CHANGEDAY":638.5299999999988,
        "CHANGEPCTDAY":6.644356828381764,
        "SUPPLY":16881800,
        "MKTCAP":173015490752,
        "TOTALVOLUME24H":470883.0751374748,
        "TOTALVOLUME24HTO":4791892728.888281
     }
   }
},
"DISPLAY":{  
  "BTC":{  
     "USD":{  
        "FROMSYMBOL":"Ƀ",
        "TOSYMBOL":"$",
        "MARKET":"CryptoCompare Index",
        "PRICE":"$ 10,248.6",
        "LASTUPDATE":"Just now",
        "LASTVOLUME":"Ƀ 0.1456",
        "LASTVOLUMETO":"$ 1,489.14",
        "LASTTRADEID":"203305344",
        "VOLUMEDAY":"Ƀ 92,548.5",
        "VOLUMEDAYTO":"$ 924,032,126.8",
        "VOLUME24HOUR":"Ƀ 107,957.6",
        "VOLUME24HOURTO":"$ 1,072,399,848.6",
        "OPENDAY":"$ 9,610.11",
        "HIGHDAY":"$ 10,409.3",
        "LOWDAY":"$ 9,411.82",
        "OPEN24HOUR":"$ 9,466.87",
        "HIGH24HOUR":"$ 10,414.1",
        "LOW24HOUR":"$ 9,396.22",
        "LASTMARKET":"Bitfinex",
        "CHANGE24HOUR":"$ 781.77",
        "CHANGEPCT24HOUR":"8.26",
        "CHANGEDAY":"$ 638.53",
        "CHANGEPCTDAY":"6.64",
        "SUPPLY":"Ƀ 16,881,800.0",
        "MKTCAP":"$ 173.02 B",
        "TOTALVOLUME24H":"Ƀ 470.88 K",
        "TOTALVOLUME24HTO":"$ 4,791.89 M"
         }
      }
   }
}

谢谢!

我认为你需要切换到较低级别 JSON API 鉴于设计不佳 API 其中有许多可能的键名称:每种货币类型一个。真的,你只想要第一个最里面的结构,剥掉外壳。

这 JSON 真的很糟糕,数字类型被埋在带有奇怪空格的字符串类型中。有人就是不明白。 “47.9189 亿美元”??? 在地球上,我们称之为:4791890000 [编辑:实际上那是 JSON 的显示部分,但这只是糟糕的另一种方式。完全浪费。]

如果您是新手,这是一个令人困惑的学习示例。

参见:https://grokswift.com/json-swift-4/

低层 API 的本质是 而不是 从 JSON 数据中立即创建一个漂亮的 Swift 对象,而是 key-value 对的字典,您必须在其中仔细检查并验证是否存在您想要的每个键以及每个值都是您想要的类型。但是,您可以获得完全的灵活性。

正在将数据提取到字典中:

if let outerJSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any], ...

现在你可以这样说了:

// crypto, currency are the input Strings in your code above
if let cryptoJSON: [String: Any] = outerJSON["RAW"] {
     if let currencyJSON: [String: Any] = cryptoJSON[crypto] {
        if let actualJSON: [String: Any] = currencyJSON[currency] {
           let myActualData = TradingData(actualJSON) // Correct O-O
           // or to show low level example:
           if let price = actualJSON["PRICE"] as? Double {
             // Avoid the DISPLAY portion of the JSON and you're OK
             // Also the above code implies a problem with the View.
             // You probably don't want to maintain an output for every
             // combination of crypto and fiat currency.
             // Unless you're using a visual data structure that *grows*
             // like a UITable. Perhaps:
             MainViewController.currencyLabel.text = currency
             MainViewController.cryptoLabel.text = crypto
             MainViewController.conversionRateLabel.text = "\(price)"
           }
        }
     }
}