How to get json fields? - json

I follow a lesson from one course
And I need to get json, but i want get another json than in a lesson.
So this is my json:
https://api.scryfall.com/cards/search?q=half
And code:
struct Card {
var cardId: String
var name: String
var imageUrl: String
var text: String
init?(dict: [String: AnyObject]){
guard let name = dict["name"] as? String,
let cardId = dict["cardId"] as? String,
let imageUrl = dict["imageUrl"] as? String,
let text = dict["text"] as? String else { return nil }
self.cardId = cardId
self.name = name
self.imageUrl = imageUrl
self.text = text
}
}
class CardNetworkService{
private init() {}
static func getCards(url: String, completion: #escaping(GetCardResponse) -> ()) {
guard let url = URL(string: url) else { return }
NetworkService.shared.getData(url: url) { (json) in
do {
print ("ok1")
let response = try GetCardResponse(json: json)
print ("ok2")
completion(response)
} catch {
print(error)
}
}
}
}
class NetworkService {
private init() {}
static let shared = NetworkService()
func getData(url: URL, completion: #escaping (Any) -> ()) {
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
DispatchQueue.main.async {
completion(json)
}
print(json)
} catch {
print(error)
}
}.resume()
}
}
struct GetCardResponse{
let cards: [Card]
init(json: Any) throws {
guard let array = json as? [[String: AnyObject]] else { throw NetworkError.failInternetError }
var cards = [Card]()
for dictionary in array {
guard let card = Card(dict: dictionary) else { continue }
cards.append(card)
}
self.cards = cards
}
}
Problem in struct GetCardResponse and [[String: AnyObject]] because I dont know how to parse this type of json. I tried to change them in the likeness of json. But I dont really understand how it works and in which part of code i need to put json["data"] or something like this... Help pls. I just want get json fields tcgplayer_id, name, art_crop

As of your code, you can parse the required details as:
struct Card {
var cardId: String = ""
var name: String = ""
var imageUrl: String = ""
var text: String = ""
init(dict: [String: Any]) {
if let obj = dict["name"] {
self.name = "\(obj)"
}
if let obj = dict["tcgplayer_id"] {
self.cardId = "\(obj)"
}
if let obj = dict["image_uris"] as? [String:Any], let url = obj["art_crop"] {
self.imageUrl = "\(url)"
}
if let obj = dict["oracle_text"] {
self.text = "\(obj)"
}
}
static func models(array: [[String:Any]]) -> [Card] {
return array.map { Card(dict: $0) }
}
}
class CardNetworkService{
private init() {}
static func getCards(url: String, completion: #escaping([Card]?) -> ()) {
guard let url = URL(string: url) else { return }
NetworkService.shared.getData(url: url) { (json) in
print ("ok1")
if let jData = json as? [String:Any], let data = jData["data"] as? [[String:Any]] {
let response = Card.models(array: data)
completion(response)
}
completion(nil)
}
}
}
class NetworkService {
private init() {}
static let shared = NetworkService()
func getData(url: URL, completion: #escaping (Any) -> ()) {
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
DispatchQueue.main.async {
completion(json)
}
} catch {
print(error)
}
}.resume()
}
}
CardNetworkService.getCards(url: "https://api.scryfall.com/cards/search?q=half") { (res) in
print(res ?? [])
}
Just paste this code in playground and it'll work.
Happy Coding :)

You are wrong get entry of data field.
First you need get data field in json. And parse to deeper.
Try use the code.
struct GetCardResponse{
let cards: [Card]
init(json: Any) throws {
guard let jsonObject = json as? [String: Any], let data = jsonObject["data"] as? [[String:AnyObject]] else { throw NetworkError.failInternetError }
var cards = [Card]()
for dictionary in data {
guard let card = Card(dict: dictionary) else { continue }
cards.append(card)
}
self.cards = cards
}
}
UPDATE:
init function in Card has something wrong. In your json cardId is not found
Card class maybe like this because cardId, imageUrl, text maybe not found. It is optional
struct Card {
var cardId: String?
var name: String
var imageUrl: String?
var text: String?
init?(dict: [String: AnyObject]){
guard let name = dict["name"] as? String else { return nil }
self.cardId = dict["cardId"] as? String
self.name = name
self.imageUrl = dict["imageUrl"] as? String
self.text = dict["text"] as? String
}
}

Try using Codable to parse the JSON data like so,
Create the models like,
struct Root: Decodable {
let cards: [Card]
enum CodingKeys: String, CodingKey {
case cards = "data"
}
}
struct Card: Decodable {
let tcgplayerId: Int
let name: String
let artCrop: String
}
Now parse your JSON data using,
if let data = data {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try JSONDecoder().decode(Root.self, from: data)
print(response)
} catch {
print(error)
}
}
You can access the properties in cards of response like so,
response.cards.first?.tcgplayerId

Related

Swift JSON with dynamic Keys

I am trying to parse JSON using Swift, which has dynamic keys. Tried several ways but still did not find the solution. Could you please help me ?
I am trying to parse NativeName, which is dynamic based on which language country name is present.
API: https://restcountries.com/v3.1/all
struct Objects: Codable {
let name: Name
let cca2 : String
let flag: String
}
struct Name: Codable {
let common, official: String
let nativeName: NativeName
}
struct NativeName: Codable {
var deu : Deu
}
struct Deu: Codable {
let official, common: String?
}
and here is JSON Model:
class ParsingService {
static let shared = ParsingService()
func fetchData() {
guard let url = URL(string: "https://restcountries.com/v3.1/all") else {
print("DEBUG: URL is nill")
return}
let session = URLSession.shared
let task = session.dataTask(with: url) { data, _, error in
guard let retrievedData = data, error == nil else {
print("DEBUG: Data is not available")
return}
print("DEBUG: Data is available \(retrievedData)")
guard let decodedData = self.JSONParsing(inputData: retrievedData) else {
print("DEBUG: Missing data")
return}
print("DEBUG: Data is there")
print("DEBUG: \(decodedData[0].cca2)")
print("DEBUG: \(decodedData[0].flag)")
print("DEBUG: \(decodedData[0].name.nativeName.deu.official)")
DispatchQueue.main.async {
print(decodedData.currencies)
}
}
task.resume()
}
func JSONParsing(inputData: Data)-> [Objects]? {
let decoder = JSONDecoder()
do {
let data = try? decoder.decode([Objects].self, from: inputData)
return data
} catch {
print("DEBUG: Cannot get data")
return nil
}
}
}
you could try this approach:
struct ContentView: View {
var body: some View {
Text("testing")
.onAppear {
fetchData() { results in
print("---> results: \(results.count) \n")
for i in 0..<3 {
print("---> results[\(i)]: \(results[i].name.nativeName)")
}
}
}
}
// todo deal with errors
func fetchData(completion: #escaping ([Objects]) -> Void) {
let url = URL(string: "https://restcountries.com/v3.1/all")
guard let url = url else { completion([]); return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { completion([]); return }
do {
let results = try JSONDecoder().decode([Objects].self, from: data)
completion(results)
}
catch {
print("Error: \(error)")
completion([])
}
}.resume()
}
}
struct Objects: Codable {
let name: Name
let cca2 : String
let flag: String
}
struct Deu: Codable {
let official, common: String?
}
struct Name: Codable {
let common, official: String
let nativeName: NativeName? // <-- here
}
// -- here
struct NativeName: Codable {
var lang: [String: Deu]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
lang = try container.decode([String: Deu].self)
}
func encode(to encoder: Encoder) throws {
// todo
}
}
Note, you could also use a Tuple, such as var lang: (key: String, value: Deu)

How parse a json data that has a dictionary key:value pair? I'm trying to extract the symbol and value pair

How to correctly parse the following json data?
My problem is at the line: if let rates = data["rates"] as? NSDictionary...
The json data
{ "valid": true,
"timestamp": 1579683079,
"base": "USD",
"rates": {
"AED": 3.67316,
"AFN": 77.99911,
"ALL": 110.11741,
...
"ZAR": 14.45,
"ZMW": 14.63257
}
}
Code
import UIKit
struct CurrencyRate {
var valid: String
var timestamp: Int
var base: String
var rates: [String:Double] = [:]
}
class ViewController: UIViewController {
var mySymbols:[String] = []
var myValues:[Double] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let jsonUrlString = "https://currencyapi.net/api/v1/rates?key=6b171cc58787d922eb53e3684d97784d165a&base=USD"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
//let dataAsString = String(data: data, encoding: .utf8)
if let rates = data["rates"] as? NSDictionary {
for (key, value) in rates {
self.mySymbols.append((key as? String)!)
self.myValues.append((value as? Double)!)
}
}
}.resume()
}
}
The syntax
for (key, value) in ...
to enumerate a dictionary exists only for native Swift dictionaries.
This is supposed to work, if you cast to specific [String:Double] you even get rid of the ugly type cast of key and value.
if let rates = data["rates"] as? [String:Double] {
for (key, value) in rates {
self.mySymbols.append(key)
self.myValues.append(value)
}
}
However you are encouraged to use the Decodable protocol to parse the JSON. There are only a few slightly changes.
struct CurrencyRate : Decodable {
let valid: Bool // must be Bool
let timestamp: Date
let base: String
let rates: [String:Double]
}
var rates = [String:Double]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let jsonUrlString = "https://currencyapi.net/api/v1/rates?key=6b171cc58787d922eb53e3684d97784d165a&base=USD"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, error) in
guard let error = error else { print(error); return }
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(CurrencyRate.self, from: data!)
self.rates = result.rates
print(rates)
} catch {
print(error)
}
}.resume()
}
timestamp is decoded as Date
Don't use NSDictionary - use Dictionary type in Swift. Also, you have data and it's not dictionary at all. It's Data type. Use should convert your data to your type. Try use this code:
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
let jsonResult = try? JSONSerialization.jsonObject(with: data)
if let dict = jsonResult as? Dictionary<String, Any>, let rates = dict["rates"] as? Dictionary<String, Double> {
for (key, value) in rates {
print(key, value)
self.mySymbols.append(key)
self.myValues.append(value)
}
}
}.resume()
There is a way with Codable, but I guess it will be the next challenge for you. Also, keep in mind that the code is executed asynchronously and the data in global variables will not appear immediately

Parsing JSON with Swift 4 Decodable

I have been getting the following error every time I try to parse JSON in my program. I can't seem to figure it out.
"Expected to decode String but found an array instead.", underlyingError: nil
Here is the code I have been struggling with:
struct Book: Decodable {
let id: Int
let title: String
let chapters: Int
var pages: [Page]?
}
struct Page: Decodable {
let id: Int
let text: [String]
}
struct Chapter: Decodable {
var chapterNumber: Int
}
func fetchJSON() {
let urlString = "https://api.myjson.com/bins/kzqh3"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { (data, _, err) in
if let err = err {
print("Failed to fetch data from", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
let books = try decoder.decode([Book].self, from: data)
books.forEach({print($0.title)})
} catch let jsonErr {
print("Failed to parse json:", jsonErr)
}
}.resume()
}
Are you sure that's the real error message?
Actually the error is supposed to be
"Expected to decode String but found a dictionary instead."
The value for key text is not an array of strings, it's an array of dictionaries
struct Page: Decodable {
let id: Int
let text: [[String:String]]
}
The struct Chapter is not needed.
Alternatively write a custom initializer and decode the dictionaries containing the chapter number as key and the text as value into an array of Chapter
struct Book: Decodable {
let id: Int
let title: String
let chapters: Int
let pages: [Page]
}
struct Page: Decodable {
let id: Int
var chapters = [Chapter]()
private enum CodingKeys : String, CodingKey { case id, chapters = "text" }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
var arrayContainer = try container.nestedUnkeyedContainer(forKey: .chapters)
while !arrayContainer.isAtEnd {
let chapterDict = try arrayContainer.decode([String:String].self)
for (key, value) in chapterDict {
chapters.append(Chapter(number: Int(key)!, text: value))
}
}
}
}
struct Chapter: Decodable {
let number : Int
let text : String
}
This is working:
import UIKit
class ViewController: UIViewController {
private var books = [Book]()
struct Book: Decodable {
let id: Int
let title: String
let chapters: Int
var pages: [Page]
}
struct Page: Decodable {
let id: Int
let text: [[String:String]]
}
override func viewDidLoad() {
super.viewDidLoad()
fetchJSON()
}
func fetchJSON() {
let urlString = "https://api.myjson.com/bins/kzqh3"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if error != nil {
print(error!.localizedDescription)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
self.books = try decoder.decode([Book].self, from: data)
DispatchQueue.main.async {
for info in self.books {
print(info.title)
print(info.chapters)
print(info.pages[0].id)
print(info.pages[0].text)
print("-------------------")
}
}
} catch let jsonErr {
print("something wrong after downloaded: \(jsonErr) ")
}
}.resume()
}
}
// print
//Genesis
//50
//1
//[["1": "In the beg...]]
//-------------------
//Exodus
//40
//2
//[["1": "In the beginning God created...]]
//
If you need to print the value of each chapter in the book, I can use something like this:
import UIKit
class ViewController: UIViewController {
private var books = [Book]()
struct Book: Decodable {
let id: Int
let title: String
let chapters: Int
var pages: [Page]
}
struct Page: Decodable {
let id: Int
let text: [[String:String]]
}
override func viewDidLoad() {
super.viewDidLoad()
fetchJSON()
}
func fetchJSON() {
let urlString = "https://api.myjson.com/bins/kzqh3"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
if error != nil {
print(error!.localizedDescription)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
self.books = try decoder.decode([Book].self, from: data)
DispatchQueue.main.async {
for info in self.books {
print(info.title)
print(info.chapters)
print(info.pages[0].id)
//print(info.pages[0].text)
for cc in info.pages[0].text {
for (key, value) in cc {
print("\(key) : \(value)")
}
}
print("-------------------")
}
}
} catch let jsonErr {
print("something wrong after downloaded: \(jsonErr) ")
}
}.resume()
}
}
//Genesis
//50
//1
//1 : In the beginning God ...
//2 : But the earth became waste...
//.
//.
//.
//31 : And God saw everything...
//-------------------
//Exodus
//40
//2
//1 : In the beginning God...
//2 : But the earth became...
//.
//.
//.
//31 : And God saw everything

Encode/Decode JSON locally from documentDirectory

I am fairly new to SWIFT and am trying to use data from a JSON file (financetoday.json) to populate a table and for the user to update and have the data stored on the device. The table has collapsing cells (using XIB) and each cell has an embedded UISlider to update the values. On my initial work, I have the table loading the JSON file successfully from the bundle, populating the table, and the slider changes each value. Now the hard part. In order to save/change the data I need to move the JSON file into the documentDirectory, then have any changes to the data from this file. Once the user starts the app for the first time, I no longer need to use the JSON file in the bundle, just the version in the documentDirectory. I have been unable to get table to read the JSON file in the documentDirectory. Any help would be appreciated.
Here is where I have added a method in AppDelegate to move the JSON file in the document Directory
// Move json file from bundle to documents folder
var finalDatabaseURL:String = String()
func copyDatabaseIfNeeded() {
let fileManager = FileManager.default
let documentsUrl = fileManager.urls(for: .documentDirectory,
in: .userDomainMask)
guard documentsUrl.count != 0 else {
return // Could not find documents URL
}
let finalDatabaseURL = documentsUrl.first!.appendingPathComponent("financetoday.json")
if !( (try? finalDatabaseURL.checkResourceIsReachable()) ?? false) {
print("DB does not exist in documents folder")
let documentsURL = Bundle.main.resourceURL?.appendingPathComponent("financetoday.json")
do {
try fileManager.copyItem(atPath: (documentsURL?.path)!, toPath: finalDatabaseURL.path)
} catch let error as NSError {
print("Couldn't copy file to final location! Error:\(error.description)")
}
} else {
print("Database file found at path: \(finalDatabaseURL.path)")
}
}
Then I added to applicationDidBecomeActive
self.copyDatabaseIfNeeded()
In my data model this is what it looks like loading JSON data from the bundle, but I need to change the code in method dataFromFile to use JSON file in documentDirectory...not the bundle. All my attempts to change results in a blank table. So for now I am pointing to the JSON in the bundle. Any help would be appreciated.
import Foundation
public func dataFromFile(_ filename: String) -> Data? {
#objc class TestClass: NSObject { }
let bundle = Bundle(for: TestClass.self)
if let path = bundle.path(forResource: filename, ofType: "json") {
return (try? Data(contentsOf: URL(fileURLWithPath: path)))
}
return nil
}
class Plan {
var yeardata: Int?
var incomedata = [Income]()
var expensedata = [Expense]()
var assetdata = [Asset]()
var liabilitydata = [Liability]()
var profiledata = [Profile]()
var assumptiondata = [Assumption]()
init?(data: Data) {
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let myplan = json["data"] as? [String: Any] {
if let incomedata = myplan["incomedata"] as? [[String: Any]] {
self.incomedata = incomedata.map { Income(json: $0) }
}
if let expensedata = myplan["expensedata"] as? [[String: Any]] {
self.expensedata = expensedata.map { Expense(json: $0) }
}
if let assetdata = myplan["assetdata"] as? [[String: Any]] {
self.assetdata = assetdata.map { Asset(json: $0) }
}
if let liabilitydata = myplan["liabilitydata"] as? [[String: Any]] {
self.liabilitydata = liabilitydata.map { Liability(json: $0) }
}
if let profiledata = myplan["profiledata"] as? [[String: Any]] {
self.profiledata = profiledata.map { Profile(json: $0) }
}
if let assumptiondata = myplan["assumptiondata"] as? [[String: Any]] {
self.assumptiondata = assumptiondata.map { Assumption(json: $0) }
}
}
} catch {
print("Error deserializing JSON: \(error)")
return nil
}
}
}
class Income {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Expense {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Asset {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Liability {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Profile {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
class Assumption {
var key: String?
var value: Any?
init(json: [String: Any]) {
self.key = json["key"] as? String
self.value = json["value"] as Any
}
}
This will read in the json. The dictionary conversion I am less familiar with because I have started using the Codable protocol which I highly recommend.
if let path = Bundle.main.path(forResource: "FileName", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
let jsonString = String(data: value, encoding: .utf8)
print("json as string: \(jsonString)")
let json = try JSONSerialization.data(withJSONObject: data, options: []) as? [String: Any]
print("json as dictionary: \(json)")
} catch let error {
print("parse error: \(error.localizedDescription)")
}
}
How to decode codable Data:
let decoder = JSONDecoder()
do {
let decodableJSON = try decoder.decode(ObjectConformingToCodable.self, from: data)
print(decodableJSON)
} catch let error {
print(error.localizedDescription)
}
not sure if this is relevant but could try:
let documentsDirectoryPathString = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let documentsDirectoryPath = NSURL(string: documentsDirectoryPathString)!
let jsonFilePath = documentsDirectoryPath.appendingPathComponent("test.json")
let fileManager = FileManager.default
var isDirectory: ObjCBool = false
// creating a .json file in the Documents folder
if fileManager.fileExists(atPath: (jsonFilePath?.absoluteString)!, isDirectory: &isDirectory) {
print("File exists")
}
I also found: JSONSaveLoad.swift on Gist:
https://gist.github.com/norsez/aa3f11c0e875526e5270e7791f3891fb
I'm sure there are other similar examples on Github

About swift Parsing JSON

I have a question.I'm trying to parse JSON.However,I got a error. It's called "Cannot call value of non-function type '[Movie]' " in this line "completionHandler(modelList)" Can you help me please?
This my class;
class Movie {
var type : String?
var title: String?
var thumbnail: String?
var id: String?
init(Dictionary : [String:AnyObject]){
self.title = Dictionary["Title"] as? String
self.id = Dictionary["Id"] as? String
self.type = Dictionary["Type"] as? String
}
static func fetchData(completionHandler: ([Movie])) -> () {
let urlString = "http://tcm-api.azurewebsites.net/api/movie/search?term='Batman'&page=1"
NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: urlString)!) { (data, response, error) -> Void in
if error != nil {
print(error)
return
}
do {
var modelList = [Movie]()
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers)
for resultDictionary in json["Data"]!!["Results"] as! [[String : AnyObject]] {
let model = Movie(Dictionary: resultDictionary)
modelList.append(model)
}
dispatch_async(dispatch_get_main_queue()) {
completionHandler(modelList)
}
} catch {
print(error)
}
}.resume()
}
}
If you look closely at your function declaration:
func fetchData(completionHandler:([Movie]))->() {
which is equal to:
func fetchData(completionHandler: [Movie]) {
What you want is this:
func fetchData(completionHandler: [Movie] -> Void) {