3

I have read How to decode a nested JSON struct with Swift Decodable protocol? It does not address my specific use case where string literal number values are used as root dictionaries.

Also How to decode a nested JSON struct with Swift Decodable protocol? answer from Imanou Petit. Can't decode JSON data from API answer from Leo Dabus.

The currencies are dictionaries themselves represented by literal string numbers inside of the data dictionary so this is throwing me off. I am looking for the most Swifty 4 model using enums where it is easy to see what containers correspond to what dictionaries.

p.s. David Berry gave a great answer below which I have implemented. If anyone else has other approaches to get to the same result I'd love to see different suggestions. Maybe there are some newer Swift 4 methods that are not well known yet or other design patterns.

Code

struct RawServerResponse {

    enum RootKeys: String, CodingKey {
        case data
        case btc = "1"
        case eth = "1027"
        case iota = "1720"
        case ripple = "52"
        case neo = "1376"
        case quotes
        case USD
    }

    enum BaseKeys: String, CodingKey {
        case id, name, symbol, maxSupply = "max_supply"
    }

    enum QuotesKeys: String, CodingKey {
        case USD
    }

    enum USDKeys: String, CodingKey {
        case price, marketCap = "market_cap"
    }

    let data: String
    let id: Int
    let name: String
    let symbol: String
    let maxSupply: Double
    let price: Double
    let marketCap: Double
}

extension RawServerResponse: Decodable {

    init(from decoder: Decoder) throws {
        // data
        let container = try decoder.container(keyedBy: RootKeys.self)
        data = try container.decode(String.self, forKey: .data)

        // id
        let idContainer = try container.nestedContainer(keyedBy: BaseKeys.self, forKey: .data)
        id = try idContainer.decode(Int.self, forKey: .id) 

        // price
        let priceContainer = try container.nestedContainer(keyedBy: USDKeys.self, forKey: .USD)
        price = try priceContainer.decode(Double.self, forKey: .price)

    }

}

API / JSON https://api.coinmarketcap.com/v2/ticker/

{
"data": {
    "1": {
        "id": 1, 
        "name": "Bitcoin", 
        "symbol": "BTC", 
        "website_slug": "bitcoin", 
        "rank": 1, 
        "circulating_supply": 17041575.0, 
        "total_supply": 17041575.0, 
        "max_supply": 21000000.0, 
        "quotes": {
            "USD": {
                "price": 8214.7, 
                "volume_24h": 5473430000.0, 
                "market_cap": 139991426153.0, 
                "percent_change_1h": 0.09, 
                "percent_change_24h": 2.29, 
                "percent_change_7d": -2.44
            }
        }, 
        "last_updated": 1526699671
    }, 
    "1027": {
        "id": 1027, 
        "name": "Ethereum", 
        "symbol": "ETH", 
        "website_slug": "ethereum", 
        "rank": 2, 
        "circulating_supply": 99524121.0, 
        "total_supply": 99524121.0, 
        "max_supply": null, 
        "quotes": {
            "USD": {
                "price": 689.891, 
                "volume_24h": 2166100000.0, 
                "market_cap": 68660795252.0, 
                "percent_change_1h": 0.13, 
                "percent_change_24h": 2.51, 
                "percent_change_7d": 2.54
            }
        }, 
        "last_updated": 1526699662
    }
}
Edison
  • 11,881
  • 5
  • 42
  • 50

1 Answers1

1

I'd take a simpler approach to the data, view the "data" as a collection of keyed, identical responses, likewise, the "quotes" is a keyed collection of Quotes.

struct RawServerResponse : Decodable {
    enum Keys : String, CodingKey {
        case data = "data"
    }

    let data : [String:Base]
}

struct Base : Decodable {
    enum CodingKeys : String, CodingKey {
        case id = "id"
        case name = "name"
        case symbol = "symbol"
        case maxSupply = "max_supply"
        case quotes = "quotes"
    }

    let id : Int64
    let name : String
    let symbol : String
    let maxSupply : Double?
    let quotes : [String:Quote]
}

struct Quote : Decodable {
    enum CodingKeys : String, CodingKey {
        case price = "price"
        case marketCap = "market_cap"
    }

    let price :  Double
    let marketCap : Double
}

Then, if you really need to access the individual keyed values out of those simpler structures, you can provide computed accessors:

extension RawServerResponse {
    enum BaseKeys : String {
        case btc = "1"
        case eth = "1027"
    }

    var eth : Base? { return data[BaseKeys.eth.rawValue] }
    var btc : Base? { return data[BaseKeys.btc.rawValue] }
}

And, likewise you could create similar accessors for currencies:

extension Base {
    enum Currencies : String {
        case usd = "USD"
    }

    var usd : Quote? { return quotes[Currencies.usd.rawValue]}
}

Once you have this part settled, then this link from your original question will show you how to flatten the structure if that's what you want. Essentially it boils down to changing the computed properties into let properties that you would set as part of the constructor.

David Berry
  • 40,941
  • 12
  • 84
  • 95
  • Yeah, those parts I just typed into the answer instead of copying from Xcode :) and the var statements need return inserted at the obvious place. – David Berry May 20 '18 at 17:34
  • 1
    I updated the extensions to be on the correct classes and syntax. – David Berry May 20 '18 at 17:35
  • 1
    Works like a charm. Thanks. Took me a second to realize that "flattened out" meant creating value properties and initializer. – Edison May 21 '18 at 04:35
  • Forgot a question. If I have an object that contains all the data from the API i.e. `let allData = response.apiData` how do I loop through only the "symbol" key? Reason I ask is because with this API each "symbol" exists in a separate currency dictionary within a parent root data dictionary. Currently I can only retrieve separate currency symbols via `btcSymbol = rawResponse.btc?.symbol`. Thanks – Edison May 21 '18 at 16:35
  • The easiest way is to use map: `for symbol in resp.data.values.map({ $0.symbol })` – David Berry May 22 '18 at 23:40
  • Although it sounds like you probably want to use the first technique in [this answer](https://stackoverflow.com/a/44556178/3203487) to restructure the data and change the key of `apiData` to `symbol` instead of an arbitrary number. – David Berry May 22 '18 at 23:43