5

Intent:

Receive cryptocurrency price data via Coinmarketcap API, decode it into custom structs in SWIFT and potentially store that data in a database (either CoreData or SQLite).

Context:

I am receiving the following error on JSONDecoder().decode:

Error serializing json: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "status", intValue: nil), _DictionaryCodingKey(stringValue: "credit_count", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))

Questions:

  1. How to properly interpret that error? What am I decoding wrong?
  2. Is the data I am receiving correctly formatted? Doesn't look like proper JSON.

The code:

import UIKit
import PlaygroundSupport


// Defining structures

struct RootObject: Decodable {
    let status: [String: StatusObject?]
    let data: DataObject?
}

struct StatusObject: Decodable {
    let credit_count: Int?
    let elapsed: Int?
    let error_code: Int?
    let timestamp: String?
}

struct DataObject: Decodable {
    let amount: Int?
    let id: Int?
    let last_updated: String?
    let name: String?
    let quote: [QuoteObject]?
    let symbol: String?
}

struct QuoteObject: Decodable {
    let usd: String?
}

struct usdObject: Decodable {
    let last_updated: String?
    let price: String?
}


//Configuring URLSession

let config = URLSessionConfiguration.default

config.httpAdditionalHeaders = ["X-CMC_PRO_API_KEY": "<removed>",
                                "Accept": "application/json",
                                "Accept-Encoding": "deflate, gzip"]

let session = URLSession(configuration: config)

let url = URL(string: "https://sandbox-api.coinmarketcap.com/v1/tools/price-conversion?convert=USD&amount=1&symbol=BTC")!


//Making and handling a request

let task = session.dataTask(with: url) { data, response, error in

    guard error == nil else {
        print ("error: \(error!)")
        return
    }

    guard let content = data else {
        print("No data")
        return
    }

    //Serializing and displaying the received data
    guard let json = (try? JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [String: Any]
    else {
        print("Not containing JSON")
        return
    }

    print(json)

    //Trying to decode
    do {
        let prices = try JSONDecoder().decode(RootObject.self, from: data!)
        print(prices)
    } catch let decodeError {
        print("Error serializing json:", decodeError)
    }
}

task.resume()

The data response and the error:

["status": {
    "credit_count" = 1;
    elapsed = 6;
    "error_code" = 0;
    "error_message" = "<null>";
    timestamp = "2019-02-16T11:10:22.147Z";
}, "data": {
    amount = 1;
    id = 1;
    "last_updated" = "2018-12-22T06:08:23.000Z";
    name = Bitcoin;
    quote =     {
        USD =         {
            "last_updated" = "2018-12-22T06:08:23.000Z";
            price = "3881.88864625";
        };
    };
    symbol = BTC;
}]
Error serializing json: typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "status", intValue: nil), _DictionaryCodingKey(stringValue: "credit_count", intValue: nil)], debugDescription: "Expected to decode Dictionary<String, Any> but found a number instead.", underlyingError: nil))

Edit 1:

Properly serialized JSON:

{
    "status": {
        "timestamp": "2019-02-16T18:54:05.499Z",
        "error_code": 0,
        "error_message": null,
        "elapsed": 6,
        "credit_count": 1
    },
    "data": {
        "id": 1,
        "symbol": "BTC",
        "name": "Bitcoin",
        "amount": 1,
        "last_updated": "2018-12-22T06:08:23.000Z",
        "quote": {
            "USD": {
                "price": 3881.88864625,
                "last_updated": "2018-12-22T06:08:23.000Z"
            }
        }
    }
}
K307
  • 105
  • 7

1 Answers1

2

There are a lot of issues in the structs.

The main issue is that the value for data is a dictionary which is decoded into a struct rather than into another dictionary. Other issues are that the type of id is String and price is Double.

APIs like Coinmarketcap send reliable data so don't declare everything as optional. Remove the question marks.

The structs below are able to decode the JSON. The quotes are decoded into a dictionary because the keys change. Add the .convertFromSnakeCase key decoding strategy to get camelCased keys. The dates are decoded as Date by adding an appropriate date decoding strategy.

I removed all those redundant ...Object occurrences except DataObject because the Data struct already exists.

struct Root: Decodable {
    let status: Status
    let data: DataObject
}

struct Status: Decodable {
    let creditCount: Int
    let elapsed: Int
    let errorCode: Int
    let timestamp: Date
}

struct DataObject: Decodable {
    let amount: Int
    let id: String
    let lastUpdated: Date
    let name: String
    let quote: [String:Quote]
    let symbol: String
}

struct Quote: Decodable {
    let lastUpdated: Date
    let price: Double
}


//Trying to decode
do {
    let dateFormatter = DateFormatter()
    dateFormatter.locale = Locale(identifier: "en_US_POSIX")
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    decoder.dateDecodingStrategy = .formatted(dateFormatter)
    let result = try decoder.decode(Root.self, from: data!)
    let quotes = result.data.quote
    for (symbol, quote) in quotes {
        print(symbol, quote.price)
    }
} catch {
    print(error)
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Thanks a lot, @vadian! Also, the key `DataObject.id` needed to be changed from `String` to `Int` – K307 Feb 16 '19 at 18:38
  • I used the sample response on the linked site where `id` is clearly a string (value in double quotes). And the documentation says it's string, too. By the way: If you specify to get a UNIX timestamp as date you can omit the custom date formatter. – vadian Feb 16 '19 at 18:40
  • Correct, they show it as String there, but it produced an error: `typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), CodingKeys(stringValue: "id", intValue: nil)], debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil))` Can it be an error in the Docs? – K307 Feb 16 '19 at 18:44
  • Could be. The actual data counts. Please ask the operator of the service – vadian Feb 16 '19 at 18:48