Wizzardzz
Wizzardzz

Reputation: 841

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

I am fetching data from an API like so:

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

As you can see I can get different things through the same struct: btc or xrp in usd or eur depending on the parameters passed in the calling function (not included to try to keep the code as short as possible).

When I want to access the Double value that the API returns here is what I do:

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

Everything works great but I have a big optimization problem here: here is what I need to do to get both btc and xrp in usd and eur:

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 }

Now that would be fine if I only had these four, but I need to fetch 25 different cryptos in 5 different currencies + their percentage change over different time frames.

I guess you see the point now.

What can I do to dynamically replace btc in let price = cryptoInfo.raw.btc?.usd?.price or eur in let price = cryptoInfo.raw.xrp?.eur?.price by passing arguments to the function, or maybe there is a different approach to avoid repetition I couldn't think of?

Example JSON input:

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

Thank you!

Upvotes: 1

Views: 164

Answers (1)

BaseZen
BaseZen

Reputation: 8718

I think you need to switch to the lower level JSON API given the poorly designed API where there are dozens of possible key names: one for each currency type. Really, you only want the first innermost structure, and peel off the outer shells.

This JSON is truly awful, what with numeric types buried in string types with bizarre spaces. Someone Just Does Not Understand. "$ 4,791.89 M" ??? On planet Earth, we call that: 4791890000 [EDIT: Actually that's the DISPLAY part of the JSON, but that's just a different way of being awful. Totally wasteful.]

If you're a novice, this is a confusing learning example.

See: https://grokswift.com/json-swift-4/

The essence of the lower level API is that it does not create a nice Swift object instantly out of JSON data, but rather a dictionary of key-value pairs, where you have to step carefully and verify that every key that you want is present and every value is the type you want. However, you get total flexibility.

Extracting the data into a Dictionary:

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

Now you can say things like:

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

Upvotes: 1

Related Questions