How to parse a JSON starting with a nameless array? - json

I'm very new to parsing (and coding)
This is my JSON:
[
{
"id": "BTC",
"currency": "BTC",
"symbol": "BTC",
"name": "Bitcoin",
"logo_url": "https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/btc.svg",
"rank": "1",
"price": "8890.83451549",
"price_date": "2020-05-05T00:00:00Z",
"price_timestamp": "2020-05-05T17:07:00Z",
"market_cap": "163265390419"
}
]
I'm trying to get a hold on the property price with the following:
struct GetPriceArray: Codable {
let getPriceArray: [GetPrice]
}
struct GetPrice: Codable {
let price: String
}
The problem is that the JSON path is 0.price (as indicated by JSON Viewer Awesome) and with the above, it is not possible to get a hold of that 0.
I already tried jumping directly to .price but it does not work.

Delete
struct GetPriceArray: Codable {
let getPriceArray: [GetPrice]
}
and decode an array
let result = JSONDecoder().decode([GetPrice].self, from: ...
Get the price with
result.first?.price

If you want GetPriceArray working with decoder and non-key JSON.
struct GetPriceArray: Codable {
let list: [GetPrice]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [GetPrice] = []
elements.reserveCapacity(container.count ?? 0)
while !container.isAtEnd {
if let element = try? container.decode(GetPrice.self) {
elements.append(element)
}
}
self.list = elements
}
}
Test.
guard let jsonData = jsonString.data(using: .utf8),
let getPriceArray = try? JSONDecoder().decode(GetPriceArray.self, from: jsonData) else {
return
}
for i in getPriceArray.list {
print(i.price)
}

Related

Swift: How to decode a JSON with a dictionary of dictionaries with each key being an incrementing number value?

I am new to Swift and I was assigned a task to decode this JSON:
{
"data": {
"id": 1,
"elements": {
"E01": {
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
"E02": {
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
}
}
}
I played around with this and I figured out how to decode this JSON when elements is an array of dictionaries with no incrementing key value. This is when elements looks like this:
"elements": [
{
"title": "cars",
"items": ["honda", "toyota", "mercedes"],
"details": {
"id": 2,
"location": "toronto"
}
},
{
"title": "bagel types",
"items": ["plain", "grain", "toasted"],
}
]
Here is my code on how to decode the JSON when elements look like the JSON directly above:
The classes I am decoding to:
public class StructureContainer: Codable {
let data: DataStructure
init(data: DataStructure) {
self.data = data
}
}
public class DataStructure: Codable {
let id: Int64?
let elements: [ElementStructure]?
init(id: Int64?, elements: [ElementStructure]?) {
self.id = id
self.elements = elements
}
}
public class ElementStructure: Codable {
let title: String?
let items: [String]?
let details: DetailStructure?
init(title: String?, items: [String]?, details: DetailStructure?) {
self.title = title
self.items = items
self.details = details
}
}
public class DetailStructure: Codable {
var id: Int64?
var location: String?
init(id: Int64?, location: String?) {
self.id = id
self.location = location
}
}
How I am decoding this:
func parseJSONLocally() {
do {
// JSONText is the variable that contains the JSON string
let jsonData = JSONText.data(using: .utf8)!
// dataInstance is the variable of type Structure that stores the parsed object.
dataInstance = try JSONDecoder().decode(StructureContainer.self, from: jsonData)
}
catch {
print("error", error)
}
}
I am able to successfully decode this when elements is an array of dictionaries with no incrementing key. However, I am lost on how to decode this when elements is a dictionary of dictionaries with each key incrementing.
I have found similar questions to this on StackOverflow but I don't think I have seen my exact problem. I am also new to Swift so I might be missing some knowledge somewhere. Any help is appreciated.
First of all Int64 makes no sense. On all modern computers Int64 is equal to Int. And declare only the properties as optional which can be nil
Assuming the dictionary keys don't matter implement init(from decoder to decode the dictionary version, decode [String:ElementStructure] and assign the dictionary values sorted by the keys to elements
public class DataStructure: Decodable {
let id: Int
let elements: [ElementStructure] // why not just Element?
private enum CodingKeys : String, CodingKey { case id, elements }
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
let elementData = try container.decode([String:ElementStructure].self, forKey: .elements)
let sortedkeys = elementData.keys.sorted{ $0.localizedStandardCompare($1) == .orderedAscending }
elements = sortedkeys.map{elementData[$0]!}
}
}

How to serialize JSON string to multidimensional NSDictionary

"[{\"person\":\"person1\",\"data\":{\"age\":\"10\",\"name\":\"John\"}},
{\"person\":\"person2\",\"data\":{\"age\":\"20\",\"name\":\"Jonathan\"}},
{\"person\":\"person3\",\"data\":{\"age\":\"30\",\"name\":\"Joe\"}}]"
Note that the value "data" is also a dictionary.
I have a JSON string like above and am trying to serialize like:
if let dataFromString = conf.data(using: .utf8, allowLossyConversion: false) {
let json = try JSON(data: dataFromString)
configuration = json.dictionary ?? [:]
}
However configuration is always an empty dictionary.
You need to parse the JSON you've as an array of dictionaries of type [[String: Any]]. The better modern approach is to use Decodable model to decode the JSON.
let string = """
[
{
"person": "person1",
"data": {
"age": "10",
"name": "John"
}
},
{
"person": "person2",
"data": {
"age": "20",
"name": "Jonathan"
}
},
{
"person": "person3",
"data": {
"age": "30",
"name": "Joe"
}
}
]
"""
let data = Data(string.utf8)
struct Person: Decodable {
let person: String
let data: PersonData
}
struct PersonData: Decodable {
let age, name: String
}
do {
let people = try JSONDecoder().decode([Person].self, from: data)
print(people)
} catch { print(error) }
For the JSON String,
let conf = "[{\"person\":\"person1\",\"data\":{\"age\":\"10\",\"name\":\"John\"}},{\"person\":\"person2\",\"data\":{\"age\":\"20\",\"name\":\"Jonathan\"}},{\"person\":\"person3\",\"data\":{\"age\":\"30\",\"name\":\"Joe\"}}]"
use JSONSerialization's jsonObject(with:options:) method to get the expected response.
if let conf = str.data(using: .utf8 ) {
do {
let dict = try JSONSerialization.jsonObject(with: data, options: []) as? [[String:Any]]
print(dict)
} catch {
print(error)
}
}

How to create simple codable struct from complex json

I receive a complex json response from the API something similar to this.
{
"result": "success",
"count": 100,
"details": [{
"unnecessaryDetails": "something",
"area": {
"name": "Test1"
}
},
{
"unnecessaryDetails": "something",
"area": {
"name": "Test2"
}
},
{
"unnecessaryDetails": "something",
"area": {
"name": "Test3"
}
}
]
}
My struct is
struct Person {
var name: String
}
struct Response {
var result: String
var count: Int
var details: [Person]
}
I don't want to create properties for everything I receive from the response. I can't ask the backend developer to give the necessary details only. How to avoid unnecessary details and create struct with require details only?
You can skip intermediate arrays and dictionaries with nested containers.
struct Person : Decodable {
let name: String
}
struct Response : Decodable {
let result: String
let count: Int
let details: [Person]
enum CodingKeys: String, CodingKey { case result, count, details }
enum DetailCodingKeys: String, CodingKey { case area }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
result = try container.decode(String.self, forKey: .result)
count = try container.decode(Int.self, forKey: .count)
var detailContainer = try container.nestedUnkeyedContainer(forKey: .details)
var people = [Person]()
while !detailContainer.isAtEnd {
let areaContainer = try detailContainer.nestedContainer(keyedBy: DetailCodingKeys.self)
let person = try areaContainer.decode(Person.self, forKey: .area)
people.append(person)
}
details = people
}
}
However the effort is much bigger than adding the extra struct
struct Response : Decodable {
let result: String
let count: Int
let details: [Detail]
}
struct Detail : Decodable {
let area : Person
}
struct Person : Decodable {
let name: String
}
You can use Codable to parse only property you want to parse and rest of all will be ignored, If you want to parse json in 2 separate models, you can follow this question's answer.
Is it possible to decode single level JSON into 2 separate models?

Decoding a nested JSON Swift

I have to decode this type of JSON that is downloaded by a response.
The JSON is this, I need retrieve the "gallery" of all items
JSON: https://pastebin.com/KnEwZzxd
I have tried many solution but I am not able to create a Decode of this son.
I have posted the full code on pastebin, too.
{
"status": 200,
"data": {
"date": "2018-07-29T00:00:00.300Z",
"featured": [
{
"id": "5b56298d781e197186378f50",
"name": "Sun Tan Specialist",
"price": "1,500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/icon.png",
"png": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/png.png",
"gallery": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/gallery.jpg",
"featured": "https://image.fnbr.co/outfit/5b56298d781e197186378f50/featured.png"
},
"rarity": "epic",
"type": "outfit",
"readableType": "Outfit"
},
{
"id": "5b562af2781e19db65378f5c",
"name": "Rescue Paddle",
"price": "800",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/icon.png",
"png": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/png.png",
"gallery": "https://image.fnbr.co/pickaxe/5b562af2781e19db65378f5c/gallery.jpg",
"featured": false
},
"rarity": "rare",
"type": "pickaxe",
"readableType": "Pickaxe"
}
],
"daily": [
{
"id": "5ab1723e5f957f27504aa502",
"name": "Rusty Rider",
"price": "1,200",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/icon.png",
"png": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/png.png",
"gallery": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/gallery.jpg",
"featured": "https://image.fnbr.co/glider/5ab1723e5f957f27504aa502/featured.png"
},
"rarity": "epic",
"type": "glider",
"readableType": "Glider"
},
{
"id": "5b0e944bdb94f1a4bbc0a8e4",
"name": "Rambunctious",
"price": "500",
"priceIcon": "vbucks",
"priceIconLink": "https://image.fnbr.co/price/icon_vbucks.png",
"images": {
"icon": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/icon.png",
"png": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/png.png",
"gallery": "https://image.fnbr.co/emote/5b0e944bdb94f1a4bbc0a8e4/gallery.jpg",
"featured": false
}
]
}
}
Aside from posting the JSON Code itself, it would be useful to actually show an attempt as to how you have attempted to decode it as well ^_________^.
Anyway, the best way to tackle this issue is to use custom Structs and the Decodable Protocol to handle the JSON response.
From your JSON you will initially get a two values:
/// The Initial Response From The Server
struct Response: Decodable {
let status: Int
let data: ResponseData
}
From this we then map the 'data' to a struct called ResponseData:
/// The Data Object
struct ResponseData: Decodable{
let date: String
let featured: [Product]
let daily: [Product]
}
In this we have two variables which contain an array of identical struct which I have called Product:
/// The Product Structure
struct Product: Decodable{
let id: String
let name: String
let price: String
let priceIcon: String
let priceIconLink: String
let images: ProductImages
let rarity: String
let type: String
let readableType: String
}
Within this we have one variable which is a dictionary (images) which we then map to another struct:
/// The Data From The Product Images Dictionary
struct ProductImages: Decodable{
let icon: String
let png: String
let gallery: String
///The Featured Variable For The Product Images Can Contain Either A String Or A Boolean Value
let featured: FeaturedType
}
The issue you have with the ProductImages, is that the featured var sometimes contains a String but on others it contains a Bool value. As such we need to create a custom struct to handle the decoding of this to ensure we always get a String (I am probably not doing this right way so if someone has a better solution please say so):
/// Featured Type Returns A String Of Either The Boolean Value Or The Link To The JPG
struct FeaturedType : Codable {
let formatted: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
//1. If We Get A Standard Response We Have A String
let stringResult = try container.decode(String.self)
formatted = stringResult
} catch {
//2. On Occassions We Get An Bool
let boolResult = try container.decode(Bool.self)
formatted = String(boolResult)
}
}
}
Now that is the basic structure of your JSON so now you need to handle it. In this example I am loading the JSON from the MainBundle as I dont have the actual URL.
/// Loads & Decodes The JSON File
func retreiveJSON(){
//1. Load The JSON File From The Main Bundle
guard let jsonURL = Bundle.main.url(forResource: "sample", withExtension: ".json") else { return }
do{
//2. Get The Data From The URL
let data = try Data(contentsOf: jsonURL)
//3. Decode The JSON
let jsonData = try JSONDecoder().decode(Response.self, from: data)
//4. Extract The Data
extractDataFrom(jsonData)
}catch{
print("Error Processing JSON == \(error)")
}
}
In the above function you will notice I am calling the function extractDataFrom() which allows you to then do what you need to do with your data:
/// Extracts The Daily & Featured Products From The JSON
///
/// - Parameter jsonData: Response
func extractDataFrom(_ jsonData: Response){
//1. Get The Daily Products
let dailyProducts = jsonData.data.daily
dailyProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
//2. Get The Featured Products
let featuredProducts = jsonData.data.featured
featuredProducts.forEach { (product) in
print(product.id)
print(product.name)
print(product.price)
print(product.priceIcon)
print(product.priceIconLink)
print(product.images)
print(product.rarity)
print(product.type)
print(product.readableType)
}
}
If you wanted to save this data then all you would need to do is add the following variables under your class declaration e.g:
var featuredProducts = [Product]()
var dailyProducts = [Product]()
And in the extractDataFrom() function change the:
let dailyProducts
let featuredProducts
To:
dailyProducts = jsonData.data.daily
featuredProducts = jsonData.data.featured
Please note that this is a very crude example, and as noted, I may not be handling the 'featured' variable correctly.
Hope it helps...
Thanks to quicktype and other services converting valid json to Swift and other languages is simple. Editing it to fit your needs should be simple enough.
// To parse the JSON, add this file to your project and do:
//
// let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
import Foundation
struct Welcome: Codable {
let status: Int
let data: DataClass
}
struct DataClass: Codable {
let date: String
let featured, daily: [Daily]
}
struct Daily: Codable {
let id, name, price, priceIcon: String
let priceIconLink: String
let images: Images
let rarity, type, readableType: String?
}
struct Images: Codable {
let icon, png, gallery: String
let featured: Featured
}
enum Featured: Codable {
case bool(Bool)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Featured.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Featured"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}

How to decode variable from json when key is changing according to user input?

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?
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"
}
]
"""
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.