Swift JSONDecoder error - Expected to decode Dictionary<String, Any> but found an array instead - arrays

I'm new to Swift 5.3 and having trouble retrieving my nested JSON data.
My JSON data result looks like this:
{
"sites":[
{
"site_no":"16103000",
"station_nm":"Hanalei River nr Hanalei, Kauai, HI",
"dec_lat_va":22.1796,
"dec_long_va":-159.466,
"huc_cd":"20070000",
"tz_cd":"HST",
"flow":92.8,
"flow_unit":"cfs",
"flow_dt":"2020-08-18 07:10:00",
"stage":1.47,
"stage_unit":"ft",
"stage_dt":"2020-08-18 07:10:00",
"class":0,
"percentile":31.9,
"percent_median":"86.73",
"percent_mean":"50.77",
"url":"https:\/\/waterdata.usgs.gov\/hi\/nwis\/uv?site_no=16103000"
}
]
}
My structs look like this:
struct APIResponse: Codable {
let sites: APIResponseSites
}
struct APIResponseSites: Codable {
let station_nm: String
let stage: Float
}
And my Decode SWIFT looks like this:
let task = URLSession.shared.dataTask(with: url, completionHandler: {
data, _, error in
guard let data = data, error == nil else {
return
}
var result: APIResponse?
do {
result = try JSONDecoder().decode(APIResponse.self, from: data)
}
catch {
print("Failed to decode with error: \(error)")
}
guard let final = result else {
return
}
print(final.sites.station_nm)
print(final.sites.stage)
})
And of course, I get an error that states:
Failed to decode with error:
typeMismatch(Swift.Dictionary<Swift.String, Any>,
Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue:
"sites", intValue: nil)], debugDescription: "Expected to decode
Dictionary<String, Any> but found an array instead.", underlyingError:
nil))
I know it has to do with 'sites' returning an array (a single one) but I don't know how to fix it. Any help would be greatly appreciated.

The error message it is pretty clear you need to parse an array of objects instead of a single object.
Just change your root declaration property from
let sites: APIResponseSites
to
let sites: [APIResponseSites]

**1.** First "sites" is an array so replace
let sites: APIResponseSites
with
let sites: [APIResponseSites]()
**2.** As sites is a array collection, please print value like given below:
print(final.sites.first?.station_nm ?? "")
print(final.sites.first?.stage ?? 0.0)
Final code is here:
struct APIResponse: Codable {
let sites: [APIResponseSites]()
}
struct APIResponseSites: Codable {
let station_nm: String
let stage: Float
}
let task = URLSession.shared.dataTask(with: url, completionHandler: {
data, _, error in
guard let data = data, error == nil else {
return
}
var result: APIResponse?
do {
result = try JSONDecoder().decode(APIResponse.self, from: data)
}
catch {
print("Failed to decode with error: \(error)")
}
guard let final = result else {
return
}
print(final.sites.first?.station_nm ?? "")
print(final.sites.first?.stage ?? 0.0)
})

Related

How to Json decode API data with an array?

I am learning Swift and trying to get elevation data based on coordinates from the Open Elevation API.
I found a code to make the request and decode the data using structs.
My problem is that the API result includes the information in an array:
{"results": [{"latitude": 41.161758, "longitude": -8.583933, "elevation": 117}]}
What I have been able to program so far does save the data as an array in json.results, but only with one index including all of the data:
[API.MyResult(latitude: 41.16176, longitude: -8.583933, elevation: 117)]
("API" is the name of the file)
Here is my code:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let url = "https://api.open-elevation.com/api/v1/lookup?locations=41.161758,-8.583933"
getData(from: url)
}
private func getData(from url: String){
let task = URLSession.shared.dataTask(with: URL(string: url)!, completionHandler: {data, response, error in
guard let data = data, error == nil else {
print("error")
return
}
var result: Response?
//print(result)
do{
result = try JSONDecoder().decode(Response.self, from: data)
}
catch{
print(error.localizedDescription)
}
guard let json = result else {
return
}
print(json.results)
//print(json.results.latitude)
//print(json.results.longitude)
//print(json.results.elevation)
})
task.resume()
}
}
struct Response: Codable {
let results: [MyResult]
}
struct MyResult: Codable {
let latitude: Float
let longitude: Float
let elevation: Int
}
Trying to print out json.results.latitude leads to the error
"Value of type '[MyResult]' has no member 'latitude'"
I assume at some point, a variable has to be defined as an array.
What needs to be changed here?
result is indeed a single object, but the property results is an array (multiple objects).
A slightly different naming avoids the confusion.
Notes:
Never print literal "error" or error.localizedDescription in a Decoding context, always print the error instance.
Proceed to parse the result in the do scope
private func getData(from url: String){
guard let url = URL(string: url) else { print("Bad URL", url); return }
let task = URLSession.shared.dataTask(with: url) {data, _, error in
if let error = error { print(error); return }
do {
let response = try JSONDecoder().decode(Response.self, from: data!)
for result in response.results {
print(result.latitude)
print(result.longitude)
print(result.elevation)
}
}
catch {
print(error)
}
}
task.resume()
}

Parse a Bad JSON File

EDIT: Beautified JSON File
Following is the JSON File That I have hosted on local sever
{
"status": "success",
"error": "",
"response": "["{\"ip_id\":\"202\",\"ip_name\":\"P b \",\"small_desc\":\"Growth Hacker\",\"large_desc\":\"Sample description\",\"join_date\":\"\",\"vid_url\":\"https:\/\/www.theug.app\/user_videos\/pr_202.mp4\",\"img_url\":\"https:\/\/www.thnug.app\/user_thumbs\/s.jpg\",\"current_status\":\"0\",\"rate_per_hour\":\"1300\",\"currency\":\"\u20b9\"}","{\"ip_id\":\"217\",\"ip_name\":\"ss dd\",\"small_desc\":\"Talented\",\"large_desc\":\"Sample description2 \",\"join_date\":\"\",\"vid_url\":\"https:\/\/www.thug.app\/user_videos\/d.mp4\",\"img_url\":\"https:\/\/www.tsnug.app\/user_thumbs\/d.jpg\",\"current_status\":\"0\",\"rate_per_hour\":\"3850\",\"currency\":\"\u20b9\"}"]"}
I have made parsing using following code.
struct userData: Decodable {
let ip_id : Int
let ip_name : String
let small_desc : String
let large_desc : String
let join_date : String
let vid_url : String
let img_url : String
let current_status : Int
let rate_per_hour : Int
let currency : String
}
struct WebsiteDescription: Decodable {
let status: String?
let error: String?
let response: [userData]
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "http://0.0.0.0:8000/api-response.json"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
//perhaps check err
//also perhaps check response status 200 OK
guard let data = data else { return }
print(data)
do {
let *users* = try JSONDecoder().decode(userData.self, from: data)
print(users)
} catch let jsonErr {
print("Error serializing json:", jsonErr)
}
}.resume()
}
}
I get The error message
Error serializing json: keyNotFound(CodingKeys(stringValue: "ip_id", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"ip_id\", intValue: nil) (\"ip_id\").", underlyingError: nil))
How do I Get the array data in var users?
How can I use the var users though out the scope of my class?
First of all it's mandatory to decode the root object, WebsiteDescription
let result = try JSONDecoder().decode(WebsiteDescription.self, from: data)
Second of all, you will run into another issue: The value for key response is a nested second level JSON string rather than a JSON array.
Don't do that. The JSON should look like
{"status":"success","error":"","response":[{"ip_id":"202","ip_name":"Prss Pus","small_desc":"Growth Hacker",...
Third of all, please name structs always with starting uppercase letter.

Cloud Firestore Reading an Array

I've got a an array of alarms (as you'll see below of type Alarm) that I'm trying to store and read from the Cloud Firestore. I'm able to upload the array of alarms but I'm not able to read/decode it. When I use the code below it crashes as I try to decode the alarmArray with the error:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [], debugDescription: "Not a dictionary", underlyingError: nil))
SaveData function:
static func saveData(alarmArray: [Alarm]) {
let db = Firestore.firestore()
let firebaseAlarms = try! FirebaseEncoder().encode(alarmArray)
db.collection(K.FStore.alarmCollection).addDocument(data: [
K.FStore.userAlarms : firebaseAlarms
]) { (error) in
if let e = error {
print("Error saving: \(e)")
} else {
print("Successfully Saved")
}
}
}
LoadData function:
static func loadData() -> [Alarm] {
let db = Firestore.firestore()
var alarmArray: [Alarm] = []
db.collection(K.FStore.alarmCollection).getDocuments { (querySnapshot, error) in
if let e = error {
print("error retrieving from Firestore, \(e)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for firebaseAlarms in snapshotDocuments {
alarmArray = try! FirebaseDecoder().decode(Alarm.self, from: firebaseAlarms) //the app crashes here!
}
}
}
}
return alarmArray
}
Alarm model:
struct Alarm: Codable {
var uuid: String
var time: Time
var label: String
var repeatStatus: [DetailInfo.DaysOfWeek]
var isOn: Bool
var onSnooze: Bool
}
I'm using the CodableFirebase pod's documentation here and Firebase's documentation here, but struggling to put them together.
Please declare your model's all property as optional.
Also property name should be same as firebase.
if you need different name use coding keys.
Serialize your firebase data before decoding.
Put your decoding line on do catch block.
Also do not use firebase decoder use Swift decoder to decode the data.
guard let JSONData = try? JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) else { return }
guard let alarm = try? JSONDecoder().decode(Alarm.self, from: JSONData) else { return }
If the error shows again use manual mapping.
It worked by changing the loadData() function to this:
static func loadData() -> [Alarm] {
let db = Firestore.firestore()
var alarmArray: [Alarm] = []
db.collection(K.FStore.alarmCollection).getDocuments { (querySnapshot, error) in
if let e = error {
print("error retrieving from Firestore, \(e)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for doc in snapshotDocuments {
let data = doc.data()
if let alarm = data[K.FStore.userAlarms] {
alarmArray = try! FirebaseDecoder().decode([Alarm].self, from: alarm)
print(alarmArray)
}
}
}
}
}
return alarmArray
}

parsing array of objects to array of string elements in swift JSON object

I am trying to parse the JSON object received from Web service which gives the result as JSON object of status and data.data again is a array of objects, from this object I want to take one element on the basis of which I have to fill a tableview.
web Service results comes as
{"status":1,"data":[{"service_id":"1","service_name":"Painter"},{"service_id":"2","service_name":"Plumber"},{"service_id":"3","service_name":"Electrician"},{"service_id":"4","service_name":"Handyman"},{"service_id":"5","service_name":"Carpenter"},{"service_id":"6","service_name":"Mason"}]}
parsing in swift I did as:--
created one class
class ABC: NSObject {
var service_name:String?
var service_id : Int?
init(service_name:String,service_id:Int) {
self.service_name = service_name
self.service_id = service_id
}
let myUrl = URL(string: "services.php");
var request = URLRequest(url:myUrl!)
request.httpMethod = "POST"// Compose a query string
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil
{
print("error=\(String(describing: error))")
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json
{
let status=parseJSON["status"] as? Int
let newdata : NSDictionary = try JSONSerialization.jsonObject(with: data!, options: JSONSerialization.ReadingOptions.mutableContainers) as! NSDictionary
self.model=(newdata.value(forKey: "data") as? [ABC])!
My problem is I am getting an array of objects in self.model as service_name and service_id keys.Now I want to take out one array of strings that contains all the service_name object values.Its saying not able to convert NSArray to swift array.
As already mentioned by others use (De)codable. It's easy to use and very comfortable. No type cast and no literal keys.
Create two structs, declare the members as non-optional constants with camelCased names and omit the initializer.
struct Root : Decodable {
let status : Int
let data : [Service]
}
struct Service : Decodable {
let serviceName : String
let serviceId : String
}
Then decode the JSON in another class or struct
let myUrl = URL(string: "services.php")
var request = URLRequest(url: myUrl!)
request.httpMethod = "POST"// Compose a query string
let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error { print(error); return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let json = try decoder.decode(Root.self, from: data!)
let status = json.status
let newdata = json.data
} catch { print(error))
}
task.resume()
I would recommend to drop JSONSerialization and use Codable protocol instead with CodingKeys.
Here is a simple example to see how it works.
struct Service : Codable {
let id : Int
let name : String
// keys
private enum CodingKeys: String, CodingKey {
case id = "service_id"
case name = "service_name"
}
}
...
// assuming our data comes from server side
let jsonString = "..."
let jsonData = jsonString.data(using: .utf8)!
do {
let jsonDecoder = JSONDecoder()
let user = try jsonDecoder.decode(Service.self, from: jsonData)
print("Getting service: \(service.id) \(service.name)")
} catch {
print("Unexpected error: \(error).")
}
More documentation here.
Use native Swift type Dictionary everywhere you use NSDictionary now
Then get certain value for key by specifing key in dictionary's subscript
if let model = newData["data"] as? [ABC] {
self.model = model
}
Anyway, I would suggest you to start using Codable instead of JSONSerialization
struct Response: Decodable {
let status: Int
let data: [ABC]
}
struct ABC: Decodable {
let serviceName: String
let serviceId: String // note that `serviceId` isn’t `Int` But `String`
}
and then decode your data using JSONDecoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let response = try decoder.decode(Response.self, from: data!)
self.model = response.data
} catch { print(error) }

Error on Serialization

I'm getting this error when I try to proceed on parsing the JSON. Does anyone know what I gotta do to fix it?
Error 1: Cannot subscript a value of type 'AudiobookJSON' (aka 'Array<Dictionary<String, Any>>') with an index of type 'String'
Error on Print
File Model: Audiobook.swift:
import Foundation
struct Audiobook : Codable {
let id: Int
let descricao: String
let urlImagem: String
init(dictionary: AudiobookJSON) {
self.id = dictionary["id"] as! Int//////ERROR MESSAGE ////////
self.descricao = dictionary["descricao"] as! String/////ERROR MESSAGE
self.urlImagem = dictionary["urlImagem"] as! String////ERROR MESSAGE
}
}
Folder Networking: APIClient.swift:
import Foundation
typealias AudiobookJSON = [[String: Any]]
struct APIClient {
static func getAudiobooksAPI(completion: #escaping ([Audiobook]?) -> Void) {
let url = URL(string: "https://alodjinha.herokuapp.com/categoria")
let session = URLSession.shared
guard let unwrappedURL = url else { print("Error unwrapping URL"); return }
let dataTask = session.dataTask(with: unwrappedURL) { (data, response, error) in
guard let unwrappedDAta = data else { print("Error unwrapping data"); return }
do {
//let json = try JSONSerialization.jsonObject(with: unwrappedDAta, options: .allowFragments) as! [String:Any]
//let posts = json["data"] as? AudiobookJSON
let posts = try JSONDecoder().decode([Audiobook].self, from: unwrappedDAta)
print(posts)
completion(posts)
} catch {
print("Could not get API data. \(error), \(error.localizedDescription)")
}
}
dataTask.resume()
}
}
As I can see that AudiobookJSON is an array of key-value pairs that's why you are getting error: So you have to use codable like that:
First: you have to make Codable type struct like that(your codable struct variable names should be same as you are getting in response):
struct Audiobook: Codable {
let id: Int?
let descricao: String?
let urlImagem: String?
}
Second: when you get the response then parse directly using codale like that:
guard let unwrappedDAta = data else { print("Error unwrapping data"); return }
do {
let posts = try JSONDecoder().decode([Audiobook].self, from: unwrappedDAta)
print(posts)
completion(posts)
} catch let message {
print("JSON serialization error:" + "\(message)")
}
You can directly use the response like:
for audio in posts {
print("audio.id")
print("audio.descricao")
print("audio.urlImagem")
}

Resources