3
votes

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

1 Answers

1
votes

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.