parse JSON, iOS, Swift4 - ios

I am trying to parse some json but seem to be getting nil in the outputs.
I am not sure where I am going wrong and could use some help trying to figure this out.
struct albumInfo: Decodable {
var name: String?
var artist: String?
var url: String?
var playcount: String?
var listeners: String?
var releasedate: String?
var summary: String?
}
class SearchVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Choice = "album"
Album = "Believe"
Artist = "Cher"
let tryURL = "\(BASE_URL)\(Choice!).getinfo&api_key=\(API_KEY)&artist=\(Artist!)&album=\(Album!)&format=json"
print(tryURL)
guard let url = URL(string: tryURL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}
}.resume()
}
Here is the data as shown with the tryUrl.

First of all please conform to the naming convention that struct names start with a capital letter.
There are two major issues:
The root object is a dictionary with one key album containing the dictionary with keys name, listeners etc.
The key summary is in another dictionary for key wiki.
The structure of the JSON is very easy to identify. The body within each pair of braces ({}) represents a separate struct.
Further there is no key releasedate so this struct member has to be declared as optional, all other members can be declared as non-optional and as constants (let). url can be declared as URL for free.
Change your structs to
struct Root : Decodable {
let album : AlbumInfo
}
struct AlbumInfo: Decodable {
let name: String
let artist: String
let url: URL
let playcount: String
let listeners: String
let releasedate: String?
let wiki : Wiki
}
struct Wiki: Decodable {
let content: String
let published: String
let summary: String
}
and decode Root
let albumDescription = try JSONDecoder().decode(Root.self, from: data)
print(albumDescription.album.artist)

The first key of your response is "album", you need to parse that first.

The classes do not correspond to json, I guess you should use the following approach (new classes implement your decode, encode protocol):
class JsonInfo {
var album : albumInfo
}
do {
let albumDescription = try JSONDecoder().decode(albumInfo.self, from: data)
print(albumDescription.album.artist)
}catch let jsonErr {
print("Error seroalizing json", jsonErr)
}

Related

How can I get the document ID when using Codable in Firestore?

I want to get document id to work with it for editing and deleting document after decoding. How can I do this?
My model:
struct MoodRecord: Codable, Hashable, Identifiable {
#DocumentID var id: String?
let user: String
let date: String
let time: String
let mood: Int
}
My function:
class func getRecords <T: Decodable> (
reference: CollectionReference,
type: T.Type,
completion: #escaping (Result<[T], Error>) -> Void
) {
reference.whereField("user", isEqualTo: AuthManager.shared.getUserId() ?? "")
.getDocuments { snapshot, error in
if let documents = snapshot?.documents {
do {
let records: [T] = try documents.map { try $0.decoded(type: T.self) }
completion(.success(records))
} catch let error {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
}
My decoder:
extension QuerySnapshot {
func decoded <T: Decodable> (type: T.Type) throws -> [T] {
let objects: [T] = try documents.map { try $0.decoded(type: T.self) }
return objects
}
}
extension QueryDocumentSnapshot {
func decoded <T: Decodable> (type: T.Type) throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: data(), options: [])
let object = try JSONDecoder().decode(type.self, from: jsonData)
return object
}
}
I use only auto-ID in Firestore and want to work with them in this task. Can I do this?
You can use Firestore's Codable support to map document IDs. No need to implement a custom decoder - we've done all the hard work for you.
Here is how.
1. Create a model for your data
You already did this. Looking at the attributes in your MoodRecord struct, I assume you want to use date and time to track timestamps, and mood to capture the value of an enum. I've updated the struct accordingly:
struct MoodRecord: Codable, Hashable, Identifiable {
#DocumentID var id: String?
var user: String
var date: Date
var time: Date
var mood: Mood
}
enum Mood: String, Codable {
case great
case ok
case good
case bad
case terrible
}
2. Map data using Codable
Fetching Firestore documents and mapping them to Swift structs becomes a one-liner thanks to Codable:
docRef.getDocument(as: MoodRecord.self) { result in
// handle result
}
Here is a complete code snippet for fetching a single document:
private func fetchMoodRecord(documentId: String) {
let docRef = db.collection("moodrecords").document(documentId)
docRef.getDocument(as: MoodRecord.self) { result in
switch result {
case .success(let moodRecord):
// A MoodRecord value was successfully initialized from the DocumentSnapshot.
self.moodRecord = moodRecord
self.errorMessage = nil
case .failure(let error):
// A MoodRecord value could not be initialized from the DocumentSnapshot.
self.errorMessage = "Error decoding document: \(error.localizedDescription)"
}
}
}
3. Updating a document
To update a document using Codable, use the following code snippet:
func updateMoodRecord(moodRecord: MoodRecord) {
if let id = moodRecord.id {
let docRef = db.collection("moodRecords").document(id)
do {
try docRef.setData(from: moodRecord)
}
catch {
print(error)
}
}
}
4.Adding new documents
Adding new documents is even easier:
func addMoodRecord(moodRecord: MoodRecord) {
let collectionRef = db.collection("moodRecords")
do {
let newDocReference = try collectionRef.addDocument(from: moodRecord)
print("Mood record stored with new document reference: \(newDocReference)")
}
catch {
print(error)
}
}
More
To learn more about how to map Firestore documents using Swift's Codable protocol, including how to map advanced data types such as date, time, colors, enums, how to fetch data using snapshot listeners, and how to handle any errors that might occur during the mapping process, check out Mapping Firestore Data in Swift - The Comprehensive Guide and the accompanying sample project on GitHub

attempt at parsing JSON file in Swift 5 ("Expected to decode Array<Any> but found a dictionary instead.")

I'm trying to make a stocks app for college related work and ive done almost everything i need to do except getting the actual data of the stocks into my app, I've been trying this and researching for the past couple days but still cannot get it to work as i get the error message :
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
any help is appreciated as this is the first ever thing i've made with swift/xcode
the code in my viewController:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchPostData { (posts) in
for post in posts {
print(post.datetime!)
}
}
}
func fetchPostData(completionHandler: #escaping ([Post]) -> Void) {
let url = URL(string: "https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=<api-key>")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
let postsData = try JSONDecoder().decode([Post].self, from: data)
completionHandler(postsData)
}
catch {
let error = error
print(error)
}
}.resume()
}
}
and the other file with the variables:
struct Post: Codable {
var datetime: Int!
var open: Int!
var high: String!
var low: String!
var close: String!
var volume: String!
}
link to the json file: https://api.twelvedata.com/time_series?symbol=AAPL&interval=1min&apikey=0561a81a9baf4ae4bc65c7af9196f929
The error is speaking for itself; you are trying to decode an array but the JSON is a dictionary. You need to change your decode:
JSONDecoder().decode(Post.self, from: data)
Edit after Joakim Danielson hint for completeness of the answer:
You have also to modify your struct in order to accomodate the JSON in your response
struct Post: Decodable {
let meta: Meta
}
struct Meta: Decodable {
let symbol: String
let interval: String
let currency: String
let values: [Values]
// more data here
}
struct Values: Decodable {
// more data here
}
First of all, all values in Post are String, please note the double quotes in the JSON
struct Post: Decodable {
let datetime, open, high, low, close, volume: String
}
But the main issue is – as mentioned in the comments and Alastar's answer and indirectly stated by the error – you are ignoring the root object. You have to decode JSON always from the top. Add this struct
struct Root: Decodable {
let status: String
let values: [Post]
}
and decode
let postsData = try JSONDecoder().decode(Root.self, from: data)
completionHandler(postsData.values)

How to debug JSONDecoder when no errors show?

I'm building a Swift app and testing in an Xcode Playground. Calling the NYTimes Search API and trying to store its response in a struct. The code executes cleanly and no errors appear (I am using a do, try, catch), but I cannot print any properties from the resulting object (print(json.status)).
My hunch is that something is fishy with this line but I'm not sure what since no errors are printing from the catch statement
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
Form the URL endpoint to make the API call:
func APICall() {
let APIKey = "MY_API_KEY_GOES_HERE_BUT_IT'S_A_SECRET"
let searchTerm = "A Super Bowl Sideshow: See the Ageless Man!"
// Remove the spaces and convert them to percents
guard let encodedSearchTerm = searchTerm.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
else {
print("Error encoding search term in URL")
return
}
let url = "https://api.nytimes.com/svc/search/v2/articlesearch.json?q=" + encodedSearchTerm + "&api-key=" + APIKey
getData(from: url)
}
Data Task:
func getData(from url: String) {
//I believe something is wrong with the following line but I'm not sure what it is
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("Error loading data")
return
}
var result: NYTSearchResponse?
do {
result = try JSONDecoder().decode(NYTSearchResponse.self, from: data)
} catch {
print(error)
}
guard let json = result else {
print("Error assigning result to json")
return
}
//Try to print these from the resulting object but these commands do not print
print(json.status)
print(json.response.docs[0].abstract)
})
task.resume()
}
My NYTSearchResponse struct which mirrors the NYT API JSON response. It's pretty complicated, but I pasted the json response into https://app.quicktype.io/ to build the struct.
// MARK: - Welcome
struct NYTSearchResponse: Codable {
let status, copyright: String
let response: Response
}
// MARK: - Response
struct Response: Codable {
let docs: [Doc]
let meta: Meta
}
// MARK: - Doc
struct Doc: Codable {
let abstract: String
let webURL: String
let snippet, leadParagraph, printSection, printPage: String
let source: String
let multimedia: [Multimedia]
let headline: Headline
let keywords: [Keyword]
let pubDate: Date
let documentType, newsDesk, sectionName, subsectionName: String
let byline: Byline
let typeOfMaterial, id: String
let wordCount: Int
let uri: String
enum CodingKeys: String, CodingKey {
case abstract
case webURL = "web_url"
case snippet
case leadParagraph = "lead_paragraph"
case printSection = "print_section"
case printPage = "print_page"
case source, multimedia, headline, keywords
case pubDate = "pub_date"
case documentType = "document_type"
case newsDesk = "news_desk"
case sectionName = "section_name"
case subsectionName = "subsection_name"
case byline
case typeOfMaterial = "type_of_material"
case id = "_id"
case wordCount = "word_count"
case uri
}
}
// MARK: - Byline
struct Byline: Codable {
let original: String
let person: [Person]
let organization: String?
}
// MARK: - Person
struct Person: Codable {
let firstname: String
let middlename: String?
let lastname: String
let qualifier, title: String?
let role, organization: String
let rank: Int
}
// MARK: - Headline
struct Headline: Codable {
let main: String
let kicker, contentKicker: String?
let printHeadline: String
let name, seo, sub: String?
enum CodingKeys: String, CodingKey {
case main, kicker
case contentKicker = "content_kicker"
case printHeadline = "print_headline"
case name, seo, sub
}
}
// MARK: - Keyword
struct Keyword: Codable {
let name, value: String
let rank: Int
let major: String
}
// MARK: - Multimedia
struct Multimedia: Codable {
let rank: Int
let subtype: String
let caption, credit: String?
let type, url: String
let height, width: Int
let legacy: Legacy
let subType, cropName: String
enum CodingKeys: String, CodingKey {
case rank, subtype, caption, credit, type, url, height, width, legacy, subType
case cropName = "crop_name"
}
}
// MARK: - Legacy
struct Legacy: Codable {
let xlarge: String?
let xlargewidth, xlargeheight: Int?
}
// MARK: - Meta
struct Meta: Codable {
let hits, offset, time: Int
}
I moved the code out of the playground and it works.

How to create a Model for dynamic keys , json parsing ,swift5

I am parsing a json data and trying to create a Model but can't figure out how to achieve the title and extract properties from the json data (which I have provided), as pageids property is dynamic. Please tell me how can I create Model to extract the title property from the page using id (stored in pageids property)
link for jsonData https://en.wikipedia.org/w/api.php?exintro=&titles=canterbury%20bells&indexpageids=&format=json&pithumbsize=500&explaintext=&redirects=1&action=query&prop=extracts%7Cpageimages
I tried little a bit, below is my code but I don't think that's correct
var ID = ""
struct Document:Codable {
let batchcomplete:String
let query:Query
}
struct Query:Codable {
let normalized:[Normalized]
let pages:Pages
var pageids:[String]{
didSet{
ID = oldValue[0]
}
}
}
struct Normalized:Codable {
let from:String
let to:String // it is a name of an flower
}
struct Pages:Codable {
let id:[Pages2]
enum CodingKeys:CodingKey {
case id = "\(ID)"
}
}
struct Pages2:Codable {
let title:String // this is an official name of flower
let extract:String // this is a body
let thumbnail:Thumbnail
}
struct Thumbnail:Codable {
let source:String //this is an url for photo
}
The model to map your JSON will be something like this:
struct Document: Codable {
let batchcomplete: String
let query: Query
}
struct Query: Codable {
let normalized: [Normalized]
var pageids: [String]
let pages: [String: Page]
}
struct Normalized: Codable {
let from: String
let to: String
}
struct Page: Codable {
let title: String
let extract: String
let thumbnail: Thumbnail
}
struct Thumbnail: Codable {
let source: String
}
and you have access to each page using pageids array and pages dictionary:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(Document.self, from: Data(jsonString.utf8))
decoded.query.pageids.forEach { id in
guard let page = decoded.query.pages[id] else { return }
print(page.title)
}
} catch {
print(error)
}
However I would prefer to make a small change to the model in order to make access to pages easier. That will require to customly implement the decoding of Query struct:
struct Query: Decodable {
let normalized: [Normalized]
let pages: [Page]
enum CodingKeys: String, CodingKey {
case normalized
case pageids
case pages
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
normalized = try container.decode([Normalized].self, forKey: .normalized)
let pageids = try container.decode([String].self, forKey: .pageids)
let pagesDict = try container.decode([String: Page].self, forKey: .pages)
pages = pageids.compactMap { pagesDict[$0] }
}
}
Then, access to each page would be as simple as a loop:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(Document.self, from: Data(jsonString.utf8))
decoded.query.pages.forEach { page in
print(page.title)
}
} catch {
print(error)
}

Having trouble accessing the API via data.nba.net

I have the Url saved in the info.plist as such:
BASE_URL <-> String <-> $(BASE_URL)
and in my project's Build Settings, I added a user-defined setting as such:
BASE_URL http://data.nba.net
After setting this up, when I try to get the website into the url variable, the variable returns "". As I debug the issue, I don't see the website stored under that variable.
I am new to Swift and still learning so any comments on the way I have setup my structs will be appreciated as well.
import UIKit
struct sports_content: Decodable {
let sports_meta_expanded: sports_meta
let teams_expanded: teams
}
struct sports_meta: Decodable {
let date_time: String?
let season_meta_list: season_meta
}
struct season_meta: Decodable {
let calendar_date: Date
let season_year: Int?
let stats_season_year: Int?
let stats_season_id: Int?
let stats_season_stage: Int?
let roster_season_year: Int?
let schedule_season_year: Int?
let standings_season_year: Int?
let season_id: Int?
let display_year: String
let display_season: String
let season_stage: Int?
}
struct next: Decodable {
let url: String
}
struct teams: Decodable {
let season_year: year
let team_data: [team]
}
struct year: Decodable {
let season_year: Int?
}
struct team: Decodable {
let is_nba_team: Bool
let team_name: String
let team_nickname: String
let team_code: String
let team_abbrev: String
let city: String
let state: String
let team_short_name: String
let team_id: Int?
let conference: String
let division_id: String
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let url = Bundle.main.infoDictionary?["BASE_URL"] as? String ?? ""
guard let convertedURL = URL(string: url) else {
return
}
URLSession.shared.dataTask(with: convertedURL) { (data, response, error) in
guard let data = data else {
return
}
do{
let dataSet = try JSONDecoder().decode(sports_content.self, from: data)
print(dataSet)
} catch {
print("JSONSerialization error:", error)
}
}.resume()
}
}
A build setting is used at build / compile time and not necessarily at run time.
To get your URL into the infoDictionary, you need to add it to the Info.plist file. Double click on your Info.plist to get the view open in your Xcode, then click "Add Value" under the Editor menu, then you can add BASE_URL as the key and your URL as the value.
Try using $(BASE_URL) as the value in your Info.plist and see if your build setting gets added in at build time. :)

Resources