how to parse the key value dictionary data - swift

I have json as below.
{
"CHF": 1.0064,
"KZT": 0.0027,
"ZAR": 0.0676,
"INR": 0.0136,
"CNY": 0.1456,
"UZS": 0.0001,
"AUD": 0.7062,
"KRW": 0.0009
}
This is nothing but list of currency & their rates.
I am confused how to parse this data.
Usually I was creating Model to parse the json data as below for User data (& not for above case).
struct UserData : Decodable {
var firstName : String?
var lastName : String?
}
& while parsing I have as below.
let globalErrObj = try JSONDecoder().decode(UserData.self, from: data!)
However as this is key value data, I am confused how Model & parsing would be.

My suggestion is to decode the JSON as [String:Double] and map it to an array of a custom struct
struct Rate {
let name : String
let value : Double
}
let rates = try JSONDecoder().decode([String: Double].self, from: data!).map(Rate.init)

Related

Coinbase API parsing into Swift App returns incorrect formatting

I am using coinmarketcap api to fetch coin prices using the code down below. The data model Coin is also given below after the code as well as the JSON response. I get an error "The data couldn’t be read because it isn’t in the correct format." What should the correct formating look like?
'''
import Foundation
import SwiftUI
import Alamofire
class CryptoViewModel: ObservableObject {
func fetchData() {
let headers: HTTPHeaders = [
"Accepts": "application/json",
"X-CMC_PRO_API_KEY": "5dd693fc-6446-44c4-8aaa-75b1bfa4376f"
]
AF.request("https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest", headers: headers).response { response in
guard let data = response.data else { return }
do {
let coins = try JSONDecoder().decode([Coin].self, from: data)
print(coins)
}
catch {
print(error.localizedDescription)
}
}
}
}
'''
'''
import SwiftUI
struct Coin: Decodable {
var slug: String?
var symbol: String?
enum CodingKeys: String, CodingKey {
case slug = "slug"
case symbol = "symbol"
}
}
'''
'''
success({
data = (
{
"circulating_supply" = 18697137;
"cmc_rank" = 1;
"date_added" = "2013-04-28T00:00:00.000Z";
id = 1;
"last_updated" = "2021-05-02T14:22:02.000Z";
"max_supply" = 21000000;
name = Bitcoin;
"num_market_pairs" = 9549;
platform = "<null>";
quote = {
USD = {
"last_updated" = "2021-05-02T14:22:02.000Z";
"market_cap" = "1063000586851.752";
"percent_change_1h" = "0.09591311";
"percent_change_24h" = "-1.05109813";
"percent_change_30d" = "-4.45794679";
"percent_change_60d" = "11.80459387";
"percent_change_7d" = "14.06195861";
"percent_change_90d" = "69.54985569999999";
price = "56853.65555441735";
"volume_24h" = "40969975368.50657";
};
};
slug = bitcoin;
symbol = BTC;
tags = (
mineable,
pow,
"sha-256",
"store-of-value",
"state-channels",
"coinbase-ventures-portfolio",
"three-arrows-capital-portfolio",
"polychain-capital-portfolio",
"binance-labs-portfolio",
"arrington-xrp-capital",
"blockchain-capital-portfolio",
"boostvc-portfolio",
"cms-holdings-portfolio",
"dcg-portfolio",
"dragonfly-capital-portfolio",
"electric-capital-portfolio",
"fabric-ventures-portfolio",
"framework-ventures",
"galaxy-digital-portfolio",
"huobi-capital",
"alameda-research-portfolio",
"a16z-portfolio",
"1confirmation-portfolio",
"winklevoss-capital",
"usv-portfolio",
"placeholder-ventures-portfolio",
"pantera-capital-portfolio",
"multicoin-capital-portfolio",
"paradigm-xzy-screener"
);
"total_supply" = 18697137;
}, ...
'''
First, I think you might want to remove the API key in your example and reset it.
Regarding your question. Your response starts with a data property. To parse this you would need start your struct there as well.
So something like this should work;
struct Coins: Decodable {
let data: [Coin]
struct Coin: Decodable {
let symbol: String
let slug: String
let quote: [String: Quote]
}
struct Quote: Decodable {
let price: Double
}
}
I'm nesting the structs to preserve the namespace. Everything is pretty related. If you ever need access to one of them on it's own you could pull them out.
Also you can omit the CodingKeys, since the key is the same as your variable name. Also I don't think you need optionals there, but I'm not completely familiar with the API.
Furthermore I think you get only 1 set of data back and not an array of coins. So I would do the following here;
let coins = try JSONDecoder().decode(Coins.self, from: data)
You are parsing an entire response as an array of coins, but the actual data, that you are interested in, is located under data key, therefore you need to add an additional layer
struct Coins: Decodable {
let coins: [Coin]
}
and decode it instead of Coin
let coins = try JSONDecoder().decode(Coins.self, from: data)

JSON array to Swift Array

I'm fairly new to Swift and trying to get a JSON array that comes back from Alamofire into a normal Swift array.
var dpoParamArr = JSON([])
//Alamofire code here and I get the value back below which is Parameters
self.dpoParamArr = JSON(dictMain)["Parameters"]
print(self.dpoParamArr.arrayValue.map({($0.dictionaryValue["name"]?.stringValue)!}))
print(self.dpoParamArr.arrayValue.map({($0.dictionaryValue["value"]?.stringValue)!}))
This is the print result:
["PAY_REQUEST_ID", "CHECKSUM"]
["5FB1DDBB-D637-3DC3-C0AF-288EFF98012C", "4FD856821E0F6D238F8346175227FF04"]
How do I get my array to have a key and value
"Parameters": [
{
"name": "PAY_REQUEST_ID",
"value": "94DEE72B-F75C-453F-1280-F1B26BBFD98E"
},
{
"name": "CHECKSUM",
"value": "9171405E05C9C9B7D4B6FF497FC4AE50"
}
]
My end goal is to get the name and value variables for both JSON nodes.
It's look like you are using Alamofire with SwiftyJSON. Below is some simple way to convert json to swift array.
1.Convert Json array to swift array.
if let swiftArray = self.dpoParamArr.arrayObject as? [[String:Any]] {
// swiftArray is array of dictionary
for item in swiftArray {
// item is a dictionary and you will get its value by key
// here how to get value by key
let name = item["name"] as? String
let value = item["value"] as? String
//printing the values
print("Name is: ", name)
print("Value is: ", value)
print("=========================")
}
}
You can also use like below
for item in self.dpoParamArr.arrayValue {
//here how to get value by key
let name = item["name"].stringValue
let value = item["value"].stringValue
//printing the values
print("Name is: ", name)
print("Value is: ", value)
print("=========================")
}
You can use SwiftyJSON for parsing. SwiftyJSON is a simplified JSON parsing library that gives you clearer syntax than the built-in iOS libraries. To detail information please read its document

Decoding numerical snake_case keys with JSONDecoder

I have the following JSON object which is to be converted to an object using JSONDecoder:
{
"first_key": 3,
"image_1000x1000": "location"
}
This maps to the following Swift model:
struct Response: Decodable {
var firstKey: Int
var image1000x1000: String
}
By using JSONDecoder with the .convertFromSnakeCase option, the snake_case keys inside the JSON are transformed to camelCase by using the algorithm defined in the documentation:
This strategy follows these steps to convert JSON keys to camel-case:
Capitalize each word that follows an underscore.
Remove all underscores that aren't at the very start or end of the string.
Combine the words into a single string.
Therefore, in this case:
first_key becomes firstKey (as expected)
image_1000x1000 should become image1000x1000
However when attempting to decode this response, a keyNotFound error for the image1000x1000 key is thrown (see this live example):
let json = "{\"first_key\": 3, \"image_1000x1000\": \"location\"}".data(using: .utf8)!
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(Response.self, from: json)
print(response)
} catch let e {
print(e)
}
What is incorrect about my camel case conversion of image_1000x1000, and why can’t JSONDecoder find the corresponding key?
You can see what the algorithm is expecting by running the process in reverse; use a JSONEncoder to encoder your data and inspect the output:
struct Response: Codable {
var firstKey: Int
var image1000x1000: String
}
let test = Response(firstKey: 10, image1000x1000: "secondKey" )
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(test)
print(String(data: data, encoding: .utf8)!)
This will produce:
{"first_key":10,"image1000x1000":"secondKey"}
So if you have control over the JSON and can live with image1000x1000 as a key, then you're done. If not you'll have to do something like this:
struct Response: Codable {
var firstKey: Int
var image1000x1000: String
private enum CodingKeys: String, CodingKey {
case image1000x1000 = "image_1000x1000"
case firstKey = "first_key"
}
}
Another option is to implement a custom key encoding strategy. It may end up being less code. See KeyEncodingStrategy for more about this.
I am facing the same issue my json keys are iso639_1 and iso639_2. Without adding Coding key it will works.
make your variable optional
Here it's my Decodable model
struct CountryModel: Decodable{
var name: String
var borders: [String]
var region: String
var alpha2Code: String
var alpha3Code: String
var flag: String
var languages: [languages]
}
struct languages: Decodable {
var iso639_1: String?
var iso639_2: String?
var name: String
var nativeName: String
}
When I add optional to both underscore variables iso639_1 and iso639_2. Then it works fine may be due to Null Value!
Here, in your case add optional to your image1000x1000 variable. Like below
struct Response: Decodable {
var firstKey: Int
var image_1000x1000: String?
}
Hope it will works!

How to create nested dictionary elements in Swift?

I want to create a variable which stores this:
["messageCode": API_200, "data": {
activities = (
{
action = 1;
state = 1;
}
);
messages = (
{
body = hi;
// ...
}
);
}, "message": ]
What I have done is this:
var fullDict: Dictionary<String, AnyObject> = [:]
fullDict["messageCode"] = "API_200" as AnyObject
var data: Dictionary<String, AnyObject> = [:]
fullDict ["data"] = data as AnyObject
Is this way is correct and how I can add activities?
I would suggest to go with creating a custom Model:
struct Model {
var messageCode: String
var data: MyData
var message: String
}
struct MyData {
let activities: [Activity]
let messages: [Message]
}
struct Activity {
var action: Int
var state: Int
}
struct Message {
var body: String
// ...
}
Thus you could use it as:
let data = MyData(activities: [Activity(action: 1, state: 1)], messages: [Message(body: "hi")])
let myModel = Model(messageCode: "API_200", data: data, message: "")
However, if you -for some reason- have to declare it as a dictionary, it could be something like this:
let myDict: [String: Any] = [
"messageCode": "API_200",
"data": ["activities": [["action": 1, "state": 1]],
"messages": [["body": "hi"]]
],
"message": ""
]
which means that myDict is a dictionary contains:
messageCode string.
data as nested dictionary, which contains:
activities array of dictionaries (array of [String: Int]).
messages array of dictionaries (array of [String: String]).
message string.
One of the simplest reasons why you should go with the modeling approach is because when it comes to read from myModel, all you have to do is to use the dot . notation. Unlike working with it as a dictionary, you would have to case its values which could be a headache for some point. For instance, let's say that we want to access the first message body in data messages array:
Model:
myModel.data.messages.first?.body
Dictionary:
if let data = myDict["data"] as? [String: [[String: Any]]],
let messages = data["messages"] as? [[String: String]],
let body = messages.first?["body"] {
print(body)
}
Since you explicitly want it as [String:AnyObject]:
var dict: [String:AnyObject] = ["messageCode":"API_200" as AnyObject,
"data": ["activities": [["action":1,
"state":1]],
"messages": [["body":"hi"]]] as AnyObject,
"message": "" as AnyObject]
Basically all the root values should be typecasted as AnyObject
Or the long way:
//Activities is as Array of dictionary with Int values
var activities = [[String:Int]]()
activities.append(["action": 1,
"state": 1])
//Messages is an Array of string
var messages = [[String:String]]()
messages.append(["body" : "hi"])
//Data is dictionary containing activities and messages
var data = [String:Any]()
data["activities"] = activities
data["messages"] = messages
//Finally your base dictionary
var dict = [String:AnyObject]()
dict["messageCode"] = "API_200" as AnyObject
dict["data"] = data as AnyObject
dict["message"] = "" as AnyObject
print(dict)
Parsing this to get your data back will be hell; with all the type casts and all.
Example (lets capture action):
let action = ((dict["data"] as? [String:Any])?["activities"] as? [String:Int])?.first?.value
As you can see you need to typecast at every level. This is the problem with using dictionaries in Swift. Too much cruft.
Sure, you could use a third-party library like SwiftyJSON to reduce the above to:
let action = dict["data"]["activities"][0]["action"]
But do you want a dependency just for something as simple as this?
Instead...
If your structure is defined then create models instead; as Ahmad F's answer suggests. It will be more readable, maintainable and flexible.
...but since you asked, this is how one would do it with pure Dictionary elements.

What does .decode return in swift 4?

When you call .decode() to decode a struct, what exactly does it return?
I have look it up on the Apple Documentation, but all it says is "native format into in-memory representations." But what does this mean? Can anyone help me?
I'm asking this because my app is crashing when I get a null value from the JSON data, from this line of code:
let plantData = try decoder.decode([Plants].self, from: data)
Here is my struct:
struct Plants: Codable {
let date: String
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And Here is my Parsing code:
func parseJson() {
let url = URL(string: ebr_String)
// Load the URL
URLSession.shared.dataTask(with:url!, completionHandler: {(data, response, error) in
// If there are any errors don't try to parse it, show the error
guard let data = data, error == nil else { print(error!); return }
let decoder = JSONDecoder()
do{
let plantData = try decoder.decode([Plants].self, from: data)
print(plantData)
And Here is just a snippet of the information I am getting back:
MorrowTabbedApp.Plants(date: "2018-02-22", monthlyAVG: Optional("1210.06")), MorrowTabbedApp.Plants(date: "2018-02-23", monthlyAVG: nil)]
Here is the snippet of JSON from the web:
[
{"Date":"2018-02-21","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-22","30_Day_MA_MMBTU":"1210.06"},
{"Date":"2018-02-23","30_Day_MA_MMBTU":null}
]
The decode method of JSONDecoder is a "generic" method. It returns an instance of whatever type you specified in the first parameter of the method. In your case, it returns a [Plants], i.e. a Array<Plants>, i.e. a Swift array of Plants instances.
If it's crashing because of a null value in your JSON, then you have to identify what was null, whether it was appropriate to be null, and if so, make sure that any Plants properties associated with values that might be null should be optionals.
Given your updated answer with code snippets, I'd suggest:
// Personally, I'd call this `Plant` as it appears to represent a single instance
struct Plant: Codable {
let date: String
let monthlyAVG: String? // Or you can use `String!` if you don't want to manually unwrap this every time you use it
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
And:
do {
let plantData = try JSONDecoder().decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
Note the filter line which selects only those occurrences for which monthlyAVG is not nil.
A couple of other suggestions:
Personally, if you could, I'd rather see the web service designed to only return the values you want (those with an actual monthlyAVG) and then change the monthlyAVG property to not be an optional. But that's up to you.
If monthlyAVG is really a numeric average, I'd change the web service to not return it as a string at all, but as a number without quotes. And then change the property of Plant to be Double or whatever.
You could, if you wanted, change the date property to be a Date and then use dateDecodingStrategy to convert the string to a Date:
struct Plant: Codable {
let date: Date
let monthlyAVG: String?
enum CodingKeys : String, CodingKey {
case date = "Date"
case monthlyAVG = "30_Day_MA_MMBTU"
}
}
and
do {
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd"
decoder.dateDecodingStrategy = .formatted(formatter)
let plantData = try decoder.decode([Plant].self, from: data)
.filter { $0.monthlyAVG != nil }
print(plantData)
} catch let parseError {
print(parseError)
}
You might do this if, for example, you wanted the x-axis of your chart to actually represent time rather than an evenly spaced set of data points.