Hello I'm using the RetroAcheivements API to build a Swift Package. Here is the JSON response:
{
"recent": [
[
{
"GameID": "11310",
"ConsoleID": "12",
"ConsoleName": "PlayStation",
"Title": "Wild Arms",
"ImageIcon": "/Images/058584.png",
"LastPlayed": "2022-11-01 13:32:47",
"NumPossibleAchievements": "156",
"PossibleScore": "1075",
"NumAchieved": "15",
"ScoreAchieved": "80",
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
},
{
"GameID": "11332",
"ConsoleID": "12",
"ConsoleName": "PlayStation",
"Title": "Final Fantasy Origins",
"ImageIcon": "/Images/060249.png",
"LastPlayed": "2022-10-29 07:01:53",
"NumPossibleAchievements": "120",
"PossibleScore": "945",
"NumAchieved": "4",
"ScoreAchieved": "18",
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
},
{
"GameID": "11248",
"ConsoleID": "12",
"ConsoleName": "PlayStation",
"Title": "Xenogears",
"ImageIcon": "/Images/049196.png",
"LastPlayed": "2022-10-28 07:51:38",
"NumPossibleAchievements": "82",
"PossibleScore": "670",
"NumAchieved": 0,
"ScoreAchieved": 0,
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
},
{
"GameID": "11255",
"ConsoleID": "12",
"ConsoleName": "PlayStation",
"Title": "Suikoden",
"ImageIcon": "/Images/054686.png",
"LastPlayed": "2022-10-28 04:55:52",
"NumPossibleAchievements": "92",
"PossibleScore": "800",
"NumAchieved": "6",
"ScoreAchieved": "30",
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
},
{
"GameID": "11320",
"ConsoleID": "12",
"ConsoleName": "PlayStation",
"Title": "Gran Turismo",
"ImageIcon": "/Images/060107.png",
"LastPlayed": "2022-10-16 13:40:43",
"NumPossibleAchievements": "68",
"PossibleScore": "585",
"NumAchieved": 0,
"ScoreAchieved": 0,
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
},
{
"GameID": "319",
"ConsoleID": "3",
"ConsoleName": "SNES",
"Title": "Chrono Trigger",
"ImageIcon": "/Images/063507.png",
"LastPlayed": "2022-10-11 15:50:15",
"NumPossibleAchievements": "77",
"PossibleScore": "600",
"NumAchieved": 0,
"ScoreAchieved": 0,
"NumAchievedHardcore": 0,
"ScoreAchievedHardcore": 0
}
]
]
}
And here is my Swift model struct generated using Quicktype.io:
struct Recents: Codable {
let recent: [[Recent]]
}
struct Recent: Codable {
let gameID, consoleID, consoleName, title: String
let imageIcon, lastPlayed: String
let numPossibleAchievements, possibleScore: Achieved
let numAchieved, scoreAchieved: Achieved
let numAchievedHardcore, scoreAchievedHardcore: Achieved
enum CodingKeys: String, CodingKey {
case gameID = "GameID"
case consoleID = "ConsoleID"
case consoleName = "ConsoleName"
case title = "Title"
case imageIcon = "ImageIcon"
case lastPlayed = "LastPlayed"
case numPossibleAchievements = "NumPossibleAchievements"
case possibleScore = "PossibleScore"
case numAchieved = "NumAchieved"
case scoreAchieved = "ScoreAchieved"
case numAchievedHardcore = "NumAchievedHardcore"
case scoreAchievedHardcore = "ScoreAchievedHardcore"
}
}
enum Achieved: Codable {
case integer(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Achieved.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Achieved"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
Here is my HTTP request code:
func getRecentGames(User: String,KEY: String,OtherUser: String){
let RECENT_URL = "https://ra.hfc-essentials.com/user_recent.php?user=+\(User)+&key=+\(KEY)+&member=\(OtherUser)&results=10&mode=json"
print(RECENT_URL)
let task = session.dataTask(with: URLRequest(url: URL(string: RECENT_URL)!)) { data, response, error in
if let error = error{
print(error)
}
if let data = data{
do{
let decodedData = try JSONDecoder().decode(Recents.self, from: data)
print(decodedData.recent.joined().first!.numAchieved)
}
catch{
print(error)
}
}
}
task.resume()
}
How can I access the NumAchievedHardcore property, which can be an Int or a String sometimes? When I decode and access them like this:
print(decodedData.recent.joined().first!.numAchieved)
I get this output:
string("15")
How can I access it normally so that it does not give string() or int() as output and instead gives only the Int or String values?
I was able to solve this issue by adding the following property to the enum which can be used to access the int value easily
var intValue: Int? {
switch self {
case .integer(let value): return value
case .string(let value): return Int(value)
}
Use This Link for Model Generate
Use Classic Swift Key/Value Dictionary
You Get Model Like This :---
/*
Copyright (c) 2022 Swift Models Generated from JSON powered by http://www.json4swift.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
/* For support, please feel free to contact me at https://www.linkedin.com/in/syedabsar */
public class Recent {
public var gameID : String?
public var consoleID : String?
public var consoleName : String?
public var title : String?
public var imageIcon : String?
public var lastPlayed : String?
public var numPossibleAchievements : String?
public var possibleScore : String?
public var numAchieved : String?
public var scoreAchieved : String?
public var numAchievedHardcore : Int?
public var scoreAchievedHardcore : Int?
/**
Returns an array of models based on given dictionary.
Sample usage:
let recent_list = Recent.modelsFromDictionaryArray(someDictionaryArrayFromJSON)
- parameter array: NSArray from JSON dictionary.
- returns: Array of Recent Instances.
*/
public class func modelsFromDictionaryArray(array:NSArray) -> [Recent]
{
var models:[Recent] = []
for item in array
{
models.append(Recent(dictionary: item as! NSDictionary)!)
}
return models
}
/**
Constructs the object based on the given dictionary.
Sample usage:
let recent = Recent(someDictionaryFromJSON)
- parameter dictionary: NSDictionary from JSON.
- returns: Recent Instance.
*/
required public init?(dictionary: NSDictionary) {
gameID = dictionary["GameID"] as? String
consoleID = dictionary["ConsoleID"] as? String
consoleName = dictionary["ConsoleName"] as? String
title = dictionary["Title"] as? String
imageIcon = dictionary["ImageIcon"] as? String
lastPlayed = dictionary["LastPlayed"] as? String
numPossibleAchievements = dictionary["NumPossibleAchievements"] as? String
possibleScore = dictionary["PossibleScore"] as? String
numAchieved = dictionary["NumAchieved"] as? String
scoreAchieved = dictionary["ScoreAchieved"] as? String
numAchievedHardcore = dictionary["NumAchievedHardcore"] as? Int
scoreAchievedHardcore = dictionary["ScoreAchievedHardcore"] as? Int
}
/**
Returns the dictionary representation for the current instance.
- returns: NSDictionary.
*/
public func dictionaryRepresentation() -> NSDictionary {
let dictionary = NSMutableDictionary()
dictionary.setValue(self.gameID, forKey: "GameID")
dictionary.setValue(self.consoleID, forKey: "ConsoleID")
dictionary.setValue(self.consoleName, forKey: "ConsoleName")
dictionary.setValue(self.title, forKey: "Title")
dictionary.setValue(self.imageIcon, forKey: "ImageIcon")
dictionary.setValue(self.lastPlayed, forKey: "LastPlayed")
dictionary.setValue(self.numPossibleAchievements, forKey: "NumPossibleAchievements")
dictionary.setValue(self.possibleScore, forKey: "PossibleScore")
dictionary.setValue(self.numAchieved, forKey: "NumAchieved")
dictionary.setValue(self.scoreAchieved, forKey: "ScoreAchieved")
dictionary.setValue(self.numAchievedHardcore, forKey: "NumAchievedHardcore")
dictionary.setValue(self.scoreAchievedHardcore, forKey: "ScoreAchievedHardcore")
return dictionary
}
}
Use This Model to fetch json data from API response Like This :----
let dataList = [Recent]()
func getRecentGames(User: String,KEY: String,OtherUser: String){
let RECENT_URL = "https://ra.hfc-essentials.com/user_recent.php?user=+\(User)+&key=+\(KEY)+&member=\(OtherUser)&results=10&mode=json"
print(RECENT_URL)
//URLSession.data
let task = URLSession.shared.dataTask(with: URLRequest(url: URL(string: RECENT_URL)!)) { data, response, error in
if let error = error{
print(error)
}
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: []) as? NSDictionary {
print("Response: \(json)")
if let data = json.value(forKey: "recent") as? [NSArray] {
self.dataList = Recent.modelsFromDictionaryArray(array: data[0])
}
print(self.dataList.first?.numAchieved)
// self.mainResponse(json)
} else {
let jsonStr = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)// No error thrown, but not NSDictionary
print("Error could not parse JSON: \(jsonStr)")
// self.eroorResponse(jsonStr!)
}
} catch let parseError {
print(parseError)// Log the error thrown by `JSONObjectWithData`
let jsonStr = NSString(data: data!, encoding: String.Encoding.utf8.rawValue)
print("Error could not parse JSON: '\(jsonStr)'")
// self.eroorResponse(jsonStr!)
}
}
task.resume()
}
Related
I am running into an issue building the correct data model for the following JSON response.
{
"resources": [
{
"courseid": 4803,
"color": "Blue",
"teeboxtype": "Championship",
"slope": 121,
"rating": 71.4
},
{
"courseid": 4803,
"color": "White",
"teeboxtype": "Men's",
"slope": 120,
"rating": 69.6
},
{
"courseid": 4803,
"color": "Red",
"teeboxtype": "Women's",
"slope": 118,
"rating": 71.2
}
]
}
Here is the current model. No matter what I do I can't seem to get the model populated. Here is also my URL session retrieving the data. I am new to Swift and SwiftUI so please be gentle. I am getting data back however I am missing something.
import Foundation
struct RatingsResources: Codable {
let golfcourserating : [GolfCourseRating]?
}
struct GolfCourseRating: Codable {
let id: UUID = UUID()
let courseID: Int?
let teeColor: String?
let teeboxtype: String?
let teeslope: Double?
let teerating: Double?
enum CodingKeysRatings: String, CodingKey {
case courseID = "courseid"
case teeColor = "color"
case teeboxtype
case teeslope = "slope"
case teerating = "rating"
}
}
func getCoureRating(courseID: String?) {
let semaphore = DispatchSemaphore (value: 0)
print("GETTING COURSE TEE RATINGS..........")
let urlString: String = "https://api.golfbert.com/v1/courses/\(courseID ?? "4800")/teeboxes"
print ("API STRING: \(urlString) ")
let url = URLComponents(string: urlString)!
let request = URLRequest(url: url.url!).signed
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let decoder = JSONDecoder()
guard let data = data else {
print(String(describing: error))
semaphore.signal()
return
}
if let response = try? JSONDecoder().decode([RatingsResources].self, from: data) {
DispatchQueue.main.async {
self.ratingresources = response
}
return
}
print("*******Data String***********")
print(String(data: data, encoding: .utf8)!)
print("***************************")
let ratingsData: RatingsResources = try! decoder.decode(RatingsResources.self, from: data)
print("Resources count \(ratingsData.golfcourserating?.count)")
semaphore.signal()
}
task.resume()
semaphore.wait()
} //: END OF GET COURSE SCORECARD
First of all, never use try? while decoding your JSON. This will hide all errors from you. Use try and an appropriate do/catch block. In the catch block at least print the error.
Looking at your model there seem to be three issues here.
You don´t have an array of RatingsResources in your array. It is just a single instance.
let response = try JSONDecoder().decode(RatingsResources.self, from: data)
RatingsResources is not implemented correct.
let golfcourserating : [GolfCourseRating]?
should be:
let resources: [GolfCourseRating]?
Your coding keys are implemented wrong instead of:
enum CodingKeysRatings: String, CodingKey {
it should read:
enum CodingKeys: String, CodingKey {
You should add enum CodingKey with resources at struct RatingsResources
And decode:
if let response = try? JSONDecoder().decode(RatingsResources.self, from: data) {
// Your response handler
}
I have some JSON I would like to decode with a JSONDecoder. Trouble is, the name of one of the properties is helpfully dynamic when sent from the server.
Like this:
{
"someRandomName": [ [1,2,3], [4,5,6] ],
"staticName": 12345
}
How can I decode this, when the someRandomName is not known at build time? I have been trawling through the www looking for an answer, but still no joy. Can't really get my head around how this Decodable, CodingKey stuff works. Some of the examples are dozens of lines long, and that doesn't seem right!
EDIT I should point out that the key is known at runtime, so perhaps I can pass it in when decoding the object?
Is there any way to hook into one of the protocol methods or properties to enable this decoding? I don't mind if I have to write a bespoke decoder for just this object: all the other JSON is fine and standard.
EDIT
Ok, my understanding has taken me this far:
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private struct CodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// just to stop the compiler moaning
pair = [[]]
last = 0
let container = try decoder.container(keyedBy: CodingKeys.self)
// how do I generate the key for the correspond "pair" property here?
for key in container.allKeys {
last = try container.decode(Int.self, forKey: CodingKeys(stringValue: "last")!)
pair = try container.decode([[Double]].self, forKey: CodingKeys(stringValue: key.stringValue)!)
}
}
}
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// this gives: "Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead.", underlyingError: nil))"
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}
So I now understand that the CodingKey conformance is generating the keys for the serialized data, not the Swift struct (which kinda makes perfect sense now I think about it).
So how do I now generate the case for pair on the fly, rather than hard-coding it like this? I know it has something to do with the init(from decoder: Decoder) I need to implement, but for the life of me I can't work out how that functions. Please help!
EDIT 2
Ok, I'm so close now. The decoding seems to be working with this:
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private enum CodingKeys : String, CodingKey {
case last
}
private struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
// Use for integer-keyed dictionary
var intValue: Int?
init?(intValue: Int) {
// We are not using this, thus just return nil
return nil
}
}
init(from decoder: Decoder) throws {
// just to stop the compiler moaning
pair = [[]]
last = 0
let container1 = try decoder.container(keyedBy: CodingKeys.self)
last = try container1.decode(Int.self, forKey: .last)
let container2 = try decoder.container(keyedBy: DynamicCodingKeys.self)
for key in container2.allKeys {
pair = try container2.decode([[Double]].self, forKey: DynamicCodingKeys(stringValue: key.stringValue)!)
}
}
}
This code seems to do its job: examining the last and pair properties in the function itself and it looks good; but I'm getting an error when trying to decode:
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [DynamicCodingKeys(stringValue: "last", intValue: nil)], debugDescription: "Expected to decode Array<Any> but found a number instead."
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}
I'm so close I can taste it...
If the dynamic key is known at runtime, you can pass it via the userInfo dictionary of the decoder.
First of all create two extensions
extension CodingUserInfoKey {
static let dynamicKey = CodingUserInfoKey(rawValue: "dynamicKey")!
}
extension JSONDecoder {
convenience init(dynamicKey: String) {
self.init()
self.userInfo[.dynamicKey] = dynamicKey
}
}
In the struct implement CodingKeys as struct to be able to create keys on the fly.
struct Pair : Decodable {
let last : Int
let pair : [[Double]]
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
static let last = CodingKeys(stringValue: "last")!
static func makeKey(name: String) -> CodingKeys {
return CodingKeys(stringValue: name)!
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let dynamicKey = decoder.userInfo[.dynamicKey] as? String else {
throw DecodingError.dataCorruptedError(forKey: .makeKey(name: "pair"), in: container, debugDescription: "Dynamic key in userInfo is missing")
}
last = try container.decode(Int.self, forKey: .last)
pair = try container.decode([[Double]].self, forKey: .makeKey(name: dynamicKey))
}
}
Now create the JSONDecoder passing the known dynamic name
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
do {
let decoder = JSONDecoder(dynamicKey: "XBTUSD")
let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
Edit:
If the JSON contains always only two keys this is an easier approach:
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
struct Pair : Decodable {
let last : Int
let pair : [[Double]]
}
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
let lastPath = codingPath.last!
if lastPath.stringValue == "last" { return lastPath }
return AnyKey(stringValue: "pair")!
})
let result = try decoder.decode(Pair.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
You're looking for JSONSerializer not JSONDecoder I guess, https://developer.apple.com/documentation/foundation/jsonserialization.
Because the key is unpredictable, so better convert to Dictionary. Or you can take a look at this https://swiftsenpai.com/swift/decode-dynamic-keys-json/
I now have some code that actually works!
struct Pair: Decodable {
var pair: [[Double]]
var last: Int
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
static let last = CodingKeys(stringValue: "last")!
static func makeKey(name: String) -> CodingKeys {
return CodingKeys(stringValue: name)!
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
last = try container.decode(Int.self, forKey: .last)
let key = container.allKeys.first(where: { $0.stringValue != "last" } )?.stringValue
pair = try container.decode([[Double]].self, forKey: .makeKey(name: key!))
}
}
init() {
let jsonString = """
{
"last": 123456,
"XBTUSD": [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0] ]
}
"""
let jsonData = Data(jsonString.utf8)
// Ask JSONDecoder to decode the JSON data as DecodedArray
let decodedResult = try! JSONDecoder().decode(Pair.self, from: jsonData)
dump(decodedResult)
}
So, the issue is - I am trying to display the Mars weather from the Mars Insight API. Here is a link Insight Weather, the data is returning in JSON format and has three levels. The keys have names that change depending on the current date (sols). How to make the structure of mutable properties?... when the property names change every day. Do we have any instruments to parse such JSON?
{
"815": {
"First_UTC": "2021-03-12T14:54:38Z",
"Last_UTC": "2021-03-13T15:34:09Z",
"Month_ordinal": 12,
"Northern_season": "late winter",
"PRE": {
"av": 728.378,
"ct": 153082,
"mn": 708.4211,
"mx": 744.9279
},
"Season": "winter",
"Southern_season": "late summer",
"WD": {
"most_common": null
}
},
"818": {
"First_UTC": "2021-03-15T20:01:49Z",
"Last_UTC": "2021-03-16T17:32:54Z",
"Month_ordinal": 12,
"Northern_season": "late winter",
"PRE": {
"av": 727.696,
"ct": 109855,
"mn": 710.223,
"mx": 743.946
},
"Season": "winter",
"Southern_season": "late summer",
"WD": {
"most_common": null
}
},
"819": {
....
,
"sol_keys": [
"815",
"818",
"819",
"820",
"821"
],
"validity_checks": {
"815": {
"PRE": {
"sol_hours_with_data": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23
],
"valid": true
}
}
So, Structure:
import Foundation
struct WeatherData: Decodable {
let season: String?
let pre: pre?
let solKeys: [String]
enum CodingKeys: String, CodingKey {
case season = "season"
case pre = "PRE"
case solKeys = "sol_keys"
}
struct pre: Decodable {
let av: Double?
let mn: Double?
let mx: Double?
enum CodingKeys: String, CodingKey {
case av = "av"
case mn = "mn"
case mx = "mx"
}
}
}
Parsing:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://api.nasa.gov/insight_weather/?api_key=DEMO_KEY&feedtype=json&ver=1.0"
guard let url = URL(string: urlString) else { return}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let safeData = data else { return }
do {
let solWeather = try JSONDecoder().decode([String:WeatherData].self, from: safeData)
if let keys = solWeather["sol_keys"] {
for key in keys { //error: For-in loop requires 'WeatherData' to conform to 'Sequence'
let report = solWeather [key]
}
}
print(solWeather)
}
catch let error {
print(error)
}
}.resume()
}
}
Got an error: For-in loop requires 'WeatherData' to conform to 'Sequence'
Don't understand why :(( Can someone help me?
Thanks!
You cannot decode [String:WeatherData].self because the dictionary contains other values which are not WeatherData, for example the [String] value of sol_keys.
The only way to decode this JSON with JSONDecoder is to implement init(with decoder, decode the sol_keys and create your own temporary CodingKeys to be able to decode the arbitrary dictionary keys.
First declare the custom CodingKey
public struct SolKeys: CodingKey {
public let stringValue: String
public init?(stringValue: String) { self.stringValue = stringValue }
public var intValue: Int? { return nil }
public init?(intValue: Int) { return nil }
}
The Decodable structs are
struct SolData : Decodable {
let firstUTC, lastUTC : Date
let pre : Pre
private enum CodingKeys : String, CodingKey {
case firstUTC = "First_UTC", lastUTC = "Last_UTC", pre = "PRE"
}
}
struct Pre: Decodable {
let av, mn, mx : Double
}
struct WeatherData: Decodable {
let solKeys: [String]
var soldata = [String:SolData]()
enum CodingKeys: String, CodingKey {
case solKeys = "sol_keys"
}
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.solKeys = try container.decode([String].self, forKey: .solKeys)
let customContainer = try decoder.container(keyedBy: SolKeys.self)
for key in solKeys {
let solKey = SolKeys(stringValue: key)!
let data = try customContainer.decode(SolData.self, forKey: solKey)
soldata[key] = data
}
}
}
And the code to receive and decode the data
let urlString = "https://api.nasa.gov/insight_weather/?api_key=DEMO_KEY&feedtype=json&ver=1.0"
let url = URL(string: urlString)!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error { print(error); return }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let solWeather = try decoder.decode(WeatherData.self, from: data!)
let keys = solWeather.solKeys
for key in keys {
let report = solWeather.soldata[key]!
print(report)
}
}
catch {
print(error)
}
}.resume()
having looked at several posts I didn't find what I was looking for. I hope this post will help me.
I use the api of CoinDesk, what I'm trying to do now is to retrieve in the answer all the codes (EUR, USD, GBP) but I can't get all the assets.
{
"time": {
"updated": "Feb 20, 2021 19:48:00 UTC",
"updatedISO": "2021-02-20T19:48:00+00:00",
"updateduk": "Feb 20, 2021 at 19:48 GMT"
},
"disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
"chartName": "Bitcoin",
"bpi": {
"USD": {
"code": "USD",
"symbol": "$",
"rate": "57,014.5954",
"description": "United States Dollar",
"rate_float": 57014.5954
},
"GBP": {
"code": "GBP",
"symbol": "£",
"rate": "40,681.1111",
"description": "British Pound Sterling",
"rate_float": 40681.1111
},
"EUR": {
"code": "EUR",
"symbol": "€",
"rate": "47,048.5582",
"description": "Euro",
"rate_float": 47048.5582
}
}
}
here's how I'm going to get the data
public class NetworkManager {
static public func fetchBPI() {
let url = "https://api.coindesk.com/v1/bpi/currentprice.json"
Alamofire.request(url).responseJSON { response in
switch response.result {
case .success:
print("✅ Success ✅")
if let json = response.data {
do {
let data = try JSON(data: json)
print(data)
let context = PersistentContainer.context
let entity = NSEntityDescription.entity(forEntityName: "BPI", in: context)
let newObject = NSManagedObject(entity: entity!, insertInto: context)
//Date Formatter
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy HH:mm"
let date = dateFormatter.date(from: data["time"]["updated"].rawValue as! String)
dateFormatter.timeZone = NSTimeZone.local
let timeStamp = dateFormatter.string(from: date ?? Date())
newObject.setValue(timeStamp, forKey: "time")
newObject.setValue(data["chartName"].rawValue, forKey: "chartName")
newObject.setValue(data["bpi"]["EUR"]["symbol"].rawValue, forKey: "symbol")
newObject.setValue(data["bpi"]["EUR"]["rate"].rawValue, forKey: "rate")
newObject.setValue(data["bpi"]["EUR"]["code"].rawValue, forKey: "code")
do {
try context.save()
print("✅ Data saved ✅")
} catch let error {
print(error)
print("❌ Saving Failed ❌")
}
}
catch {
print("❌ Error ❌")
}
}
case .failure(let error):
print(error)
}
}
}
}
I would like to get in the bpi key all the codes and put them in a list to use them.
The best way to handle the json here in my opinion is to treat the content under "bpi" as a dictionary instead.
struct CoinData: Codable {
let time: Time
let chartName: String
let bpi: [String: BPI]
}
struct BPI: Codable {
let code: String
let rate: Double
enum CodingKeys: String, CodingKey {
case code
case rate = "rate_float"
}
}
struct Time: Codable {
let updated: Date
enum CodingKeys: String, CodingKey {
case updated = "updatedISO"
}
}
I have removed some unnecessary (?) properties and also note that I made updatedISO in Time into a Date if that might be useful since it's so easy to convert it.
To properly decode this use try with a do/catch so you handle errors properly.
Here is an example of that where I also loop over the different currencies/rates
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let result = try decoder.decode(CoinData.self, from: data)
print(result.time.updated)
let coins = result.bpi.values
for coin in coins {
print(coin)
}
} catch {
print(error)
}
Output:
2021-02-20 19:48:00 +0000
BPI(code: "GBP", rate: 40681.1111)
BPI(code: "USD", rate: 57014.5954)
BPI(code: "EUR", rate: 47048.5582)
You should use objects to represent the data from the server. It would be something like this:
struct CoinData: Codable {
let time: Time
let disclaimer, chartName: String
let bpi: BPI
}
struct BPI: Codable {
let usd, gbp, eur: Eur
enum CodingKeys: String, CodingKey {
case usd = "USD"
case gbp = "GBP"
case eur = "EUR"
}
}
struct Eur: Codable {
let code, symbol, rate, eurDescription: String
let rateFloat: Double
enum CodingKeys: String, CodingKey {
case code, symbol, rate
case eurDescription = "description"
case rateFloat = "rate_float"
}
}
struct Time: Codable {
let updated: String
let updatedISO: String
let updateduk: String
}
Than when you download the data you parse it like this:
let coinData = try? JSONDecoder().decode(CoinData.self, from: jsonData)
coinData?.bpi.eur // Access EUR for instance
UPDATE:
Simple demo to demonstrate the parsing using the data you get from your server:
let dataFromServer = "{\"time\":{\"updated\":\"Feb 20, 2021 21:02:00 UTC\",\"updatedISO\":\"2021-02-20T21:02:00+00:00\",\"updateduk\":\"Feb 20, 2021 at 21:02 GMT\"},\"disclaimer\":\"This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org\",\"chartName\":\"Bitcoin\",\"bpi\":{\"USD\":{\"code\":\"USD\",\"symbol\":\"$\",\"rate\":\"56,689.8367\",\"description\":\"United States Dollar\",\"rate_float\":56689.8367},\"GBP\":{\"code\":\"GBP\",\"symbol\":\"£\",\"rate\":\"40,449.3889\",\"description\":\"British Pound Sterling\",\"rate_float\":40449.3889},\"EUR\":{\"code\":\"EUR\",\"symbol\":\"€\",\"rate\":\"46,780.5666\",\"description\":\"Euro\",\"rate_float\":46780.5666}}}"
do {
let json = try JSONDecoder().decode(CoinData.self, from: dataFromServer.data(using: .utf8)!)
print(json.bpi.eur) // Will print EUR object
} catch let error {
print(error)
}
I have trouble parsing json data from YouTube api with JSONSerialization. when I try to fetch I return error. this is my code
this is json data I want to parse, I want to get video id, url, title, and desctiption
{
"kind": "youtube#searchListResponse",
"etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/mBDPbwkuU2lLUxWHYPI1X54CUwQ\"",
"nextPageToken": "CAUQAA",
"regionCode": "ID",
"pageInfo": {
"totalResults": 3552,
"resultsPerPage": 5
},
"items": [
{
"kind": "youtube#searchResult",
"etag": "\"j6xRRd8dTPVVptg711_CSPADRfg/73cXngXOrGm_Bt7McNY945A6koc\"",
"id": {
"kind": "youtube#video",
"videoId": "-0ZZzOuuV3c"
},
"snippet": {
"publishedAt": "2018-09-20T08:00:01.000Z",
"channelId": "UCjHoMXZXAIx_QHgk9qsAJ-Q",
"title": "HADIST-HADIST PALSU TAPI POPULER - Ustadz Adi Hidayat LC MA",
"description": "\"Kebersihan sebagian dari iman\". Sering dogn mendengar ucapan ini. Sebagian orang mengatakan ini hadist dari Rasulullah. Tapi taukah kamu, bahwa ini ...",
"thumbnails": {
"default": {
"url": "https://i.ytimg.com/vi/-0ZZzOuuV3c/default.jpg",
"width": 120,
"height": 90
},
"medium": {
"url": "https://i.ytimg.com/vi/-0ZZzOuuV3c/mqdefault.jpg",
"width": 320,
"height": 180
},
"high": {
"url": "https://i.ytimg.com/vi/-0ZZzOuuV3c/hqdefault.jpg",
"width": 480,
"height": 360
}
},
"channelTitle": "Audio Dakwah",
"liveBroadcastContent": "none"
}
}
This is my code to parse json I create it in youtubeAPI struct, when I try to run it invalidJSONData
static func videos(fromJSON data: Data) -> VideoResults {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard
let jsonDictionary = jsonObject as? [AnyHashable: Any],
let itemsArray = jsonDictionary["items"] as? [[String: Any]]
else {
return .failure(YoutubeError.invalidJSONData)
}
var finalItems = [Video]()
for itemJSON in itemsArray {
if let item = video(fromJSON: itemJSON) {
finalItems.append(item)
}
}
if finalItems.isEmpty && !itemsArray.isEmpty {
return .failure(YoutubeError.invalidJSONData)
}
return .success(finalItems)
} catch let error {
return .failure(error)
}
}
private static func video(fromJSON json: [String: Any]) ->Video? {
guard
let videoID = json["videoID"] as? String,
let title = json["title"] as? String,
let description = json["description"] as? String,
let stringURL = json["url"] as? String,
let url = URL(string: stringURL)
else {
return nil
}
return Video(videoID: videoID, title: title, description: description, url: url)
}
with the question I have post. finally I have the answer for my problem. hopefully this will help other developer who want to parse json with JSONSerialization, with such a complex data like YouTube api or other api. so this is my answer.
Since items is an array and inside item have another nested data such as snippet, so I need to iterate snippet to get the data. This is the code.
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
var finalItems = [Video]()
if let items = jsonObject?["items"] as? Array<Dictionary<String, Any>> {
for item in items {
if
let id = item["id"] as? [String: String],
let snippet = item["snippet"] as? [String: Any] {
if
let videoID = id["videoId"],
let description = snippet["description"] as? String,
let title = snippet["title"] as? String,
let thumbnails = snippet["thumbnails"] as? [String: Any],
let medium = thumbnails["medium"] as? [String: Any],
let urlString = medium["url"] as? String,
let url = URL(string: urlString) {
finalItems.append(Video(videoID: videoID, title: title, description: description, url: url))
}
}
}
}
print(finalItems.count)
if finalItems.isEmpty {
return .failure(YoutubeError.invalidJSONData)
}
return .success(finalItems)
} catch let error {
return .failure(error)
}
And the the next step is to clean up the code for parsing that iterate the item, and put it in extension in Video Model, I was found a good article from apple developer website to clean up the code so the code not bloated in my videos function. if you are curious check out this link https://developer.apple.com/swift/blog/?id=37. so the extension for video model is like this.
extension Video {
init(dict: [String: Any]) throws {
guard
let id = dict["id"] as? [String: String],
let videoID = id["videoId"]
else {
throw SerializationError.missing("id")
}
guard
let snippet = dict["snippet"] as? [String: Any],
let description = snippet["description"] as? String,
let title = snippet["title"] as? String,
let thumbnails = snippet["thumbnails"] as? [String: Any],
let medium = thumbnails["medium"] as? [String: Any],
let urlString = medium["url"] as? String,
let url = URL(string: urlString) else { throw SerializationError.missing("snippet") }
self.videoID = videoID
self.title = title
self.description = description
self.url = url
}
}
and then update the videos function, and add do catch block when you try to append since the extension have potential error to parse json data. this is the update method
static func videos(fromJSON data: Data) -> VideoResults {
do {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
var finalItems = [Video]()
if let items = jsonObject?["items"] as? Array<Dictionary<String, Any>> {
for item in items {
do {
try finalItems.append(Video(dict: item))
} catch let error {
print("Failed to append video: \(error)")
}
}
}
print(finalItems.count)
if finalItems.isEmpty {
return .failure(YoutubeError.invalidJSONData)
}
return .success(finalItems)
} catch let error {
return .failure(error)
}
}
and voila the code is safe now, why I not use JSONDecoder? since a lot of people recommended to use JSONDecoder. the reason is, this is my personal reason by the way other might not agree with me. I know how to use JSONDecoder but the thing is i don't want to create a lot struct to decode the json data, since with this JSONSerialization doesn't have to create struct it help me reduce a file and step up my learning curve. and hey even a library like Alamofire have a choice to parse using JSONSerialization. it's up to you now if you want to use JSONDecoder or JSONSerialization I hope this help :).