Swift: How can I parse a JSON response from a server if it's wrapped in an array? - arrays

Swift n00b here.
I'm getting something like this as a response from a server:
[
{
"foo": [],
"bar":"asdf",
...
}
]
Now I understand how to parse regular JSON, but not when it has an array as the base element.
Here is code I used so far, which would work for regular JSON:
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
// check for any errors
guard error == nil else {
print("error calling POST on \(String(describing: urlRequest.url?.absoluteURL))")
print(error!)
return
}
// make sure we got the data
guard let responseData = data else {
print("Error: did not receive data")
return
}
let responseString = String(data: responseData, encoding: String.Encoding.utf8) as String!
// parse the result as JSON, since that's what the API provides
do {
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
as? [String: Any] else {
print("error trying to convert data to JSON")
return
}
... // do whatever with the response
} catch {
print("an error occurred")
return
}
}
task.resume()
The error I'm getting from that is "error trying to convert data to JSON.
The easiest way to parse that response I can think of is making a substring from 1 to length - 1 and then parsing it, but that doesn't seem particularly safe.
Is there any way I can parse that response into a [Dictionary]?

Your JSON is regular JSON. An array at the top level is just as valid (and regular) as a dictionary.
Simply update your cast accordingly:
guard let todo = try JSONSerialization.jsonObject(with: responseData, options: [])
as? [[String: Any]] else {
That indicates that you have an array of dictionary. Now you can iterate the array and get each dictionary as needed.

Related

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

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)
})

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.

Swift 4 SwiftyJSON: Appending JSON results as Strings to an array

I am having issues with appending JSON results onto an empty array using SwiftyJSON. In the view controller, I have the following:
var jsonResults = [String]()
func runAPI() {
let jsonUrlString = "sampleUrl"
guard let url = URL(string: jsonUrlString) else {
return
}
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
do {
let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
self.jsonResults.append(JSON(json).stringValue) // SwiftyJSON used here
print(JSON(json)) // prints a string on the console
print(JSON(json).stringValue) // prints the same string on the console
} catch let jsonError {
print("Error serializing json:", jsonError)
}
}
.resume()
}
override func viewDidLoad() {
super.viewDidLoad()
runAPI()
print(jsonResults.count) // prints "0" :(
}
I was hoping if someone had any ideas on how to get a JSON result converted into a string and appended onto an array. Any input would be greatly appreciated.

How to properly parse a dictionary array

I'm trying to parse the observed wind speed value from the following json as a float (5.586995). The code below returns the value as an array (["5.586995"]). Is this the best approach in Swift 3? Any help is much appreciated.
{
"properties":{
"platform_type_description":"Weather and Sea Surface Data",
"parameters":[
{
"id":"wind_speed",
"units":"m s-1",
"depth":{
"value":"0",
"units":"m"
},
"observations":{
"times":[
"2017-04-21T14:08:00"
],
"values":[
"5.586995"
],
"quality_levels":[
"3"
]
}
},
{
"id":"wind_speed_of_gust",
"units":"m s-1",
"depth":{
"value":"0",
"units":"m"
},
"observations":{
"times":[
"2017-04-21T14:08:00"
],
"values":[
"7.39788"
],
"quality_levels":[
"3"
]
}
}
]
}
}
func getWind() {
let url = URL(string: "http://windapp.dev/")!
URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?) in
if error == nil {
if data != nil {
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as! [String:Any]
guard let properties = json["properties"] as? [String:Any],
let parameters = properties["parameters"] as? [Any],
let windspeedDict = parameters[0] as? [String:Any],
let observationsWindSpeedDict = windspeedDict["observations"] as? [String:[String]]
else {
print("Error!")
return
}
for (key, value) in observationsWindSpeedDict {
if key == "values" {
print(value)
}
}
} catch {}
}
} else {
print("Error: Couldn't connect to server")
}
}.resume()
}
This is the expected result, since the JSON contains the float value as a value of an array with only one element and you make value the whole array when iterating through your dictionary.
If you know your data will always look exactly like this, you should make your parsing function more specific and don't save any values as Any. If this is a response from your own backend and you think your project will get quite complex, you could use a 3rd party service (for example Swagger, though it has quite a few bugs) enabling you to write a more abstract specification of your JSON and generate both server and client side code automatically. However, I would only recommend this for large and complex projects.
Other than that I think your method is an optimal approach for parsing this response.

Compiling Swift source files hangs on large array reduce-combine + expressions

In my tests I'm used to write Strings in arrays in different lines like
let jsonString = ["{"
,"\"url\": \"http://localhost:8090/rest/api/3\","
, "\"id\": \"3\","
, "\"description\": \"A test that needs to be done.\","
, "\"name\": \"Test\","
, "\"subtest\": false,"
, "\"avatar\": 1"
,"}"].reduce("", combine: +)
That works fine, still my array get 145 lines for a large test json string.
With 145 lines (or maybe less, didn't tried it line by line) the build task hangs while "Compiling Swift source files".
First, that is a bit crazy. 30 lines are ok, 145 not? What?
Second, what is a better solution to write a String in multiple lines in Swift? - I don't want to load a json and parse it from an external file.
This is probably because of type inference.
Try giving an explicit [String] type to an array variable to help the compiler figure it out, then apply reduce to the variable.
let arrayOfStrings: [String] = ["{"
,"\"url\": \"http://localhost:8090/rest/api/3\","
, "\"id\": \"3\","
, "\"description\": \"A test that needs to be done.\","
, "\"name\": \"Test\","
, "\"subtest\": false,"
, "\"avatar\": 1"
,"}"] // etc
let jsonString = arrayOfStrings.reduce("", combine: +)
Anyway, to create JSON you should make a dictionary then serialize it with NSJSONSerialization, this is less error prone:
Swift 2
do {
// Create a dictionary.
let dict = ["url": "http://localhost:8090/rest/api/3", "id": "3"] // etc
// Encode it to JSON data.
let jsonData = try NSJSONSerialization.dataWithJSONObject(dict, options: [])
// Get the object back.
if let jsonObject = try NSJSONSerialization.JSONObjectWithData(jsonData, options: []) as? [String:String] {
print(jsonObject)
}
// Get it as a String.
if let jsonString = String(data: jsonData, encoding: NSUTF8StringEncoding) {
print(jsonString)
}
} catch let error as NSError {
print(error)
}
Swift 3
do {
// Create a dictionary.
let dict = ["url": "http://localhost:8090/rest/api/3", "id": "3"] // etc
// Encode it to JSON data.
let jsonData = try JSONSerialization.data(withJSONObject: dict, options: [])
// Get the object back.
if let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String:String] {
print(jsonObject)
}
// Get it as a String.
if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
print(jsonString)
}
} catch let error as NSError {
print(error)
}

Resources