2

I am trying to parse some JSON response coming from CoinmarketCap using the JSONDecoder() in Swift 4. But the problem is that the response from json is changing according to user input. e.g if user wants the price in eur, the output is following:

[
    {
        "price_eur": "9022.9695444"
    }
]

but if user wants the price in gbp:

[
    {
        "price_gbp": "7906.8032145"
    }
]

So the question is how should I make the struct that inherits from Decodable if the variable(json key) name is changing?

Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
Tarvo Mäesepp
  • 4,477
  • 3
  • 44
  • 92

2 Answers2

5

You can decode the dynamic key by creating a custom init(from:) method for your struct, then using two set of coding keys, an enum containing all keys that are known at compile time and another struct that you initialize using the dynamic keys that are generated using user input (contain the name of the currency).

In your custom init(from:) method you just need to decode each property using their respective keys.

let chosenCurrency = "gbp"

struct CurrencyResponse: Decodable {
    let name:String
    let symbol:String
    let price:String
    private static var priceKey:String {
        return "price_\(chosenCurrency)"
    }

    private enum SimpleCodingKeys: String, CodingKey {
        case name, symbol
    }

    private struct PriceCodingKey : CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }

    init(from decoder:Decoder) throws {
        let values = try decoder.container(keyedBy: SimpleCodingKeys.self)
        name = try values.decode(String.self, forKey: .name)
        symbol = try values.decode(String.self, forKey: .symbol)
        let priceValue = try decoder.container(keyedBy: PriceCodingKey.self)
        price = try priceValue.decode(String.self, forKey: PriceCodingKey(stringValue:CurrencyResponse.priceKey)!)
    }
}

do {
    let cryptoCurrencies = try JSONDecoder().decode([CurrencyResponse].self, from: priceJSON.data(using: .utf8)!)
} catch {
    print(error)
}

Test JSON:

let priceJSON = """
    [
    {
    "id": "bitcoin",
    "name": "Bitcoin",
    "symbol": "BTC",
    "rank": "1",
    "price_\(chosenCurrency)": "573.137",
    "price_btc": "1.0",
    "24h_volume_\(chosenCurrency)": "72855700.0",
    "market_cap_\(chosenCurrency)": "9080883500.0",
    "available_supply": "15844176.0",
    "total_supply": "15844176.0",
    "percent_change_1h": "0.04",
    "percent_change_24h": "-0.3",
    "percent_change_7d": "-0.57",
    "last_updated": "1472762067"
    },
    {
    "id": "ethereum",
    "name": "Ethereum",
    "symbol": "ETH",
    "rank": "2",
    "price_\(chosenCurrency)": "12.1844",
    "price_btc": "0.021262",
    "24h_volume_\(chosenCurrency)": "24085900.0",
    "market_cap_\(chosenCurrency)": "1018098455.0",
    "available_supply": "83557537.0",
    "total_supply": "83557537.0",
    "percent_change_1h": "-0.58",
    "percent_change_24h": "6.34",
    "percent_change_7d": "8.59",
    "last_updated": "1472762062"
}
]
"""
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • That is a awesome answer, thank you very much. As I understand then the "chosenCurrency" has to be a global variable then? – Tarvo Mäesepp Jan 24 '18 at 14:25
  • @TarvoMäesepp no worries, glad I could help. No, `chosenCurrency` doesn't actually need to be a global variable, you can define it anywhere as long as you can access it both from the initializer of `CurrencyResponse` and from your viewcontroller class, where the user input is coming from. I just tested my code in a playground, so this was the easiest solution for defining `chosenCurrency`. – Dávid Pásztor Jan 24 '18 at 14:32
  • Adding `chosenCurrency` outside of the `struct` is not working very well. For instance, if you want to parse in `tableview` it will be late in updating `chosenCurrency`. – Emm Sep 04 '20 at 16:14
2

If you have a small number of possible keys, you can do the following

struct Price: Decodable {

    var value: String

    enum CodingKeys: String, CodingKey {
        case price_eur
        case price_gbp
        case price_usd
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        do {
            value = try container.decode(String.self, forKey: .price_eur)
        } catch {
            do {
                value = try container.decode(String.self, forKey: .price_gbp)
            } catch {
                value = try container.decode(String.self, forKey: .price_usd)
            }
        }
    }
}

let data = try! JSONSerialization.data(withJSONObject: ["price_gbp": "10.12"], options: [])
let price = try JSONDecoder().decode(Price.self, from: data)

Otherwise, you will need to parse data manually.

  • You don't actually need to parse the data manually, it is possible to use dynamic keys while still conforming to the `Codable` protocol. See my answer on how to do it. – Dávid Pásztor Jan 24 '18 at 13:29