Xcode 9 Swift 4 Complex JSON decoding - json

I am working with API data that returns JSON data that is hard to decode. The api call is for a batch of stock quotations. When a single quotation (not batch) is called, the result is easily decoded JSON using a simple struct. However, in batch mode the single quote version is grouped within two more levels that I can not decode. In the interest of making this easy to read I will just paste the initial pieces of the data in order to illustrate the issue.
The single quote JSON:
{"symbol":"AAPL","companyName":"Apple Inc.","primaryExchange":"Nasdaq Global Select",
So, that's easy... key, value pairs from the start but in batch mode this becomes:
{"AAPL":{"quote":{"symbol":"AAPL","companyName":"Apple Inc.","primaryExchange":"Nasdaq Global Select",
and then later in that same result would be a second or third or more quote, eg.
}},"FB":{"quote":{"symbol":"FB","companyName":"Facebook Inc.","primaryExchange":"Nasdaq Global Select",
So at the highest level it is not a key but is instead a value. And the second level is a metadata type placeholder for quote (because you can also request other subelement arrays like company, charts, etc.) I can't think of how to handle the outer grouping(s) especially the stock symbols AAPL and FB ... as the outermost elements. Any thoughts anyone?
I have started down the path of JSONSerialization which produces a string that I also cannot get into a usable form.
For this I am using:
let tkrs = "C,DFS"
var components = URLComponents()
components.scheme = "https"
components.host = "api.iextrading.com"
components.path = "/1.0/stock/market/batch"
let queryItemSymbols = URLQueryItem(name: "symbols", value: "\(tkrs)")
let queryItemTypes = URLQueryItem(name: "types", value: "quote")
components.queryItems = [queryItemSymbols,queryItemTypes]
let session = URLSession.shared
let task = session.dataTask(with: components.url!) {(data, response, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
print(json)
which produces:
["C": {
quote = {
avgTotalVolume = 17386485;
calculationPrice = tops;
change = "1.155";
changePercent = "0.0181";
close = "63.8";
closeTime = 1540411451191;
companyName = "Citigroup Inc.";
and there is more data but I'm clipping it short.
The api url's are:
single quote example:
https://api.iextrading.com/1.0/stock/aapl/quote
batch quote example:
https://api.iextrading.com/1.0/stock/market/batch?symbols=aapl,fb&types=quote
The struct I have used successfully for the single quote works nicely with a simple line of code:
let quote = try JSONDecoder().decode(Quote.self,from: data)
where Quote is a struct:
struct Quote: Decodable {
let symbol: String
let companyName: String
let primaryExchange: String
let sector: String
let calculationPrice: String
let open: Double
let openTime: Int
let close: Double
let closeTime: Int
let high: Double
let low: Double
let latestPrice: Double
let latestSource: String
let latestTime: String
let latestUpdate: Int
let latestVolume: Double
let iexRealtimePrice: Double?
let iexRealtimeSize: Double?
let iexLastUpdated: Int?
let delayedPrice: Double
let delayedPriceTime: Int
let extendedPrice: Double
let extendedChange: Double
let extendedChangePercent: Double
let extendedPriceTime: Int
let previousClose: Double
let change: Double
let changePercent: Double
let iexMarketPercent: Double?
let iexVolume: Double?
let avgTotalVolume: Double
let iexBidPrice: Double?
let iexBidSize: Double?
let iexAskPrice: Double?
let iexAskSize: Double?
let marketCap: Double
let peRatio: Double?
let week52High: Double
let week52Low: Double
let ytdChange: Double
}
Edit: based on answer provided
Working in a playground this works well with the batch data:
func getPrices(){
let tkrs = "AAPL,FB,C,DFS,MSFT,ATVI"
var components = URLComponents()
components.scheme = "https"
components.host = "api.iextrading.com" ///1.0/stock/market/batch
components.path = "/1.0/stock/market/batch"
let queryItemSymbols = URLQueryItem(name: "symbols", value: "\(tkrs)")
let queryItemTypes = URLQueryItem(name: "types", value: "quote")
components.queryItems = [queryItemSymbols,queryItemTypes]
let data = try! Data(contentsOf: components.url!)
do {
let response = try JSONDecoder().decode([String:[String: Quote]].self,from: data)
let tickers = ["AAPL","FB","C","DFS","MSFT","ATVI"]
for tk in tickers {
let quote = response[tk]
let price = quote!["quote"]
print("\(price!.symbol) \(price!.latestPrice)")
}
} catch let jsonErr { print("Error decoding json:",jsonErr)}
}
But this solves my initial problem of getting a response back from a URLSession for just a single quote. I can now run through an array of stock symbols and update the latest price for each item with this function.
func getPrice(ticker: String) -> Double {
var price = 0.0
let urlString = "https://api.iextrading.com/1.0/stock/\(ticker)/quote"
let data = try! Data(contentsOf: URL(string: urlString)!)
do {
let response = try JSONDecoder().decode(Quote.self,from: data)
price = response.latestPrice
} catch let jsonErr { print("Error decoding JSON:",jsonErr)}
return price
}
So, I am iterating through an array of open stock trades and setting the price like this...
opentrades[rn].trCurPrice = getPrice(ticker: opentrades[rn].trTicker)
And it works great in my application. Although I am a little worried about how it will workout during times of high latency. I realize I need some error control and will work to integrate that going forward.
Edit/Update: Based on feedback here is the approach I'm taking.
Created a class to be a delegate that accepts an array of open trades and updates the prices.
import Foundation
protocol BatchQuoteManagerDelegate {
func didLoadBatchQuote()
}
class BatchQuoteManager {
var openPositions = [OpenTradeDur]()
var delegate: BatchQuoteManagerDelegate? = nil
func getBatchQuote(tickers: [OpenTradeDur]) {
var tkrs = ""
for tk in tickers {
tkrs = tkrs + "\(tk.trTicker),"
}
var components = URLComponents()
components.scheme = "https"
components.host = "api.iextrading.com"
components.path = "/1.0/stock/market/batch"
let queryItemSymbols = URLQueryItem(name: "symbols", value: "\(tkrs)")
let queryItemTypes = URLQueryItem(name: "types", value: "quote")
components.queryItems = [queryItemSymbols,queryItemTypes]
let session = URLSession.shared
let task = session.dataTask(with: components.url!) {(data,response,error) in
guard let data = data, error == nil else { return }
let response = try! JSONDecoder().decode([String:[String: Quote]].self,from: data)
for i in 0..<tickers.count {
let quote = response[tickers[i].trTicker]
let price = quote!["quote"]
tickers[i].trCurPrice = price!.latestPrice
}
self.openPositions = tickers
if let delegate = self.delegate {
DispatchQueue.main.async {
delegate.didLoadBatchQuote()
}
}
}
task.resume()
}
}
I then extend my ViewController with BatchQuoteManagerDelegate, implement the func didLoadBatchQuote() method where I get the updated prices via the BatchQuoteManager.openPositions array. I just needed to define let batchQuoteManager = BatchQuoteManager() in my ViewController and within viewDidLoad() include the statement batchQuoteManager.delegate = self. Once I know that all the necessary data has been loaded into my ViewController I call the function to get prices (at the end of viewDidLoad()) with batchQuoteManager.getBatchQuote(tickers: opentrades)
And that's it. It is working very nicely so far.

The Dictionary type conditionally conforms to Decodable if its associated KeyType and ValueType conform to Decodable. You can decode the whole Dictionary.
let response = try JSONDecoder().decode([String:[String: Quote]].self,from: data)
let apple = response["AAPL"]
let appleQuote = apple["quote"]
Try this gist in a playground
https://gist.github.com/caquant/eeee66b7b8df447c4ea06b8ab8c1116a
Edit: Here is a quick example with URLSession
let session = URLSession.shared
let dataTask = session.dataTask(with: url) { (data, response, error) in
guard let data = data, error == nil else { return }
let response = try! JSONDecoder().decode([String:[String: Quote]].self,from: data)
let apple = response["FB"]
let appleQuote = apple!["quote"]
print(appleQuote!)
}
dataTask.resume()
Note: The gist was also updated.

Related

Parsing json blob field in Swift

I am reading JSON from a web service in swift which is in the following format
[{
"id":1,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"5555555",
"logo":[-1,-40,-1,-32],
"shopPath":"test"
},
{
"id":2,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"66666666",
"logo":[-1,-50,-2,-2],
"shopPath":"test"
}]
I have managed to read all the strings easily but when it comes to the logo part I am not sure what should I do about it, this is a blob field in a mySQL database which represent an image that I want to retrieve in my swift UI, here is my code for doing that but i keep getting errors and not able to figure the right way to do it:
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: [String]
let path: String
}
func getBrandsJson() {
let url = URL(string: "http://10.211.55.4:8080/exam/Test")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else {
print(error!);
return
}
print(response.debugDescription)
let decoder = JSONDecoder()
let classes = try! decoder.decode([Brand].self, from: data)
for myClasses in classes {
print(myClasses.branch)
if let imageData:Data = myClasses.logo.data(using:String.Encoding.utf8){
let image = UIImage(data:imageData,scale:1.0)
var imageView : UIImageView!
}
}
}).resume()
}
Can someone explain how to do that the right way I have searched a lot but no luck
First of all, you should better tell the server side engineers of the web service, that using array of numbers is not efficient to return a binary data in JSON and that they should use Base-64 or something like that.
If they are stubborn enough to ignore your suggestion, you should better decode it as Data in your custom decoding initializer.
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: Data
let path: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: CodingKeys.id)
self.name = try container.decode(String.self, forKey: CodingKeys.name)
self.branch = try container.decode(String.self, forKey: CodingKeys.branch)
self.address = try container.decode(String.self, forKey: CodingKeys.address)
self.phone = try container.decode(String.self, forKey: CodingKeys.phone)
self.path = try container.decode(String.self, forKey: CodingKeys.path)
//Decode the JSON array of numbers as `[Int8]`
let bytes = try container.decode([Int8].self, forKey: CodingKeys.logo)
//Convert the result into `Data`
self.logo = Data(bytes: bytes.lazy.map{UInt8(bitPattern: $0)})
}
}
And you can write the data decoding part of your getBrandsJson() as:
let decoder = JSONDecoder()
do {
//You should never use `try!` when working with data returned by server
//Generally, you should not ignore errors or invalid inputs silently
let brands = try decoder.decode([Brand].self, from: data)
for brand in brands {
print(brand)
//Use brand.logo, which is a `Data`
if let image = UIImage(data: brand.logo, scale: 1.0) {
print(image)
//...
} else {
print("invalid binary data as an image")
}
}
} catch {
print(error)
}
I wrote some lines to decode array of numbers as Data by guess. So if you find my code not working with your actual data, please tell me with enough description and some examples of actual data. (At least, you need to show me a first few hundreds of the elements in the actual "logo" array.)
Replace
let logo: [String]
with
let logo: [Int]
to get errors use
do {
let classes = try JSONDecoder().decode([Brand].self, from: data)
}
catch {
print(error)
}

Use Swift Decoder to pull attributes from JSON array

I have a JSON array created using this call:
guard let json = (try? JSONSerialization.jsonObject(with: content, options: JSONSerialization.ReadingOptions.mutableContainers)) as? [Any] else {
print("This is not JSON!!!")
return
}
I am trying to get elements from the JSON objects in the array to display them using the following code:
struct sWidget: Codable{
var createdBy: String
var createdDate: Date
var status: String
var widgetNumber: String
var updatedBy: String
var updatedDate: Date
}
do {
let decoder = JSONDecoder()
for (index, value) in json.enumerated() {
let currentWidget = try decoder.decode(sWidget.self, from: json[index] as! Data)
let currentNum = currentWidget.widgetNumber
//print(currentNum)
widgetNums.append(currentNum)
}
}
catch {
print("decoding error")
}
The code compiles but when I run it I get this error in the output:
Could not cast value of type '__NSDictionaryM' (0x1063c34f8) to
'NSData' (0x1063c1090). 2018-08-09 09:41:02.666713-0500
TruckMeterLogScanner[14259:1223764] Could not cast value of type
'__NSDictionaryM' (0x1063c34f8) to 'NSData' (0x1063c1090).
I am still investigating but any tips would be helpful.
Did you try that fetching objects like above mentioned? Because i see that you are using Codable. Fetching is very simple with that actually.
let yourObjectArray = JSONDecoder().decode([sWidget].self, data: json as! Data)
May be this line can be buggy but you can fetch them with one line.
Extending #Cemal BAYRI's answer:
JSONDecoder() throws, so make sure to either us try? or try (don't forget do-catch with try)
guard let data = content as? Data else {
return [sWidget]()
}
let jsonDecoder = JSONDecoder()
1. try?
let yourObjectArray = try? jsonDecoder.decode([sWidget].self, data: data)
2. try
do {
let yourObjectArray = try jsonDecoder.decode([sWidget].self, data: data)
} catch let error {
}
Note: You would need to take care of Data and Date formatting. Below is an example for Date:
jsonDecoder.dateDecodingStrategy = .iso8601
You can also check it out here

Swift 4 JSONSerialization.jsonObject

I am using Xcode 9.2 and Swift 4. How can I check if the returned json data is null?
Now it gives me error Type 'Any' has no subscript members at line if json[0]["data"]
var json: NSMutableArray = []
var newsArray: NSMutableArray = []
let url = URLFactory()
var data = try! NSData(contentsOf: url.getURL()) as Data
do {
json = try JSONSerialization.jsonObject(with: data, options: []) as! NSMutableArray
if json[0]["data"] {
// data is not null
}
} catch let error as NSError {
// handle error
}
My JSON returns something like this:
{
"data":
[
{
"news_id":123,
"title":"title",
"news_date":"2017-02-08 21:46:06",
"news_url":"url",
"short_description":"description",
"category_id":4,
"category_name":"Health",
"latlng":
[
{
"lat":"43.003429",
"lng":"-78.696335"
}
]
}
{ ....
}
If you're using Swift 4, I might suggest JSONDecoder:
First, define the types to hold the parsed data:
struct ResponseObject: Codable {
let data: [NewsItem]
}
struct NewsItem: Codable {
let newsId: Int
let title: String
let newsDate: Date
let newsURL: URL
let shortDescription: String
let categoryID: Int
let categoryName: String
let coordinates: [Coordinate]
// because your json keys don't follow normal Swift naming convention, use CodingKeys to map these property names to JSON keys
enum CodingKeys: String, CodingKey {
case newsId = "news_id"
case title
case newsDate = "news_date"
case newsURL = "news_url"
case shortDescription = "short_description"
case categoryID = "category_id"
case categoryName = "category_name"
case coordinates = "latlng"
}
}
struct Coordinate: Codable {
let lat: String
let lng: String
}
And then you can parse it:
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
do {
let responseObject = try decoder.decode(ResponseObject.self, from: data)
print(responseObject.data)
} catch {
print(error)
}
Clearly, if your JSON is different, you might need to change your objects accordingly (e.g. it struck me odd that latlng was an array of coordinates). Also, you can define custom init(from:) methods if you want to convert some of these strings into numbers, but I'd rather fix the JSON, instead (why are latlng returning string values rather than numeric values).
For more information, see Encoding and Decoding Custom Types.
As an aside, I'd advise against this pattern (note, this is your network request logic, but excised of NSData):
let data = try! Data(contentsOf: url.getURL())
That will retrieve the data synchronously, which can be problematic because
the app will be frozen while the data is being retrieved resulting in a poor UX;
you risk having your app killed by the watchdog process which looks for frozen apps; and
you don't have robust error handling and this will crash if the network request fails.
I'd suggest using URLSession:
let task = URLSession.shared.dataTask(with: url.getURL()) { data, _, error in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
// now parse `data` like shown above
// if you then need to update UI or model objects, dispatch that back
// to the main queue:
DispatchQueue.main.async {
// use `responseObject.data` to update model objects and/or UI here
}
}
task.resume()

Clear data in an array while requesting new json response (fatal error: Index out of range swift 3)

I have json request which show a list of cars. I used timer to refresh the request as well as the list every 10 seconds. The problem is that the data keep adding up to the array and make my application crashes. How can I clear the data before appending new data? What should I do?
let list = listdevices[indexPath.row] // error
if list.statusxe == "run" {
cell?.devnameLabel?.text = list.devname
cell?.addressLabel?.text = list.address
cell?.statusxeLabel?.textColor = UIColor(red: 1/255, green: 117/255, blue: 0/255, alpha: 1)
cell?.statusxeLabel?.text = "Đang chạy"
cell?.speedLabel?.text = "\(list.speed) km/h"
}
else if list.statusxe == "stop"{
cell?.devnameLabel?.text = list.devname
cell?.addressLabel?.text = list.address
cell?.statusxeLabel?.textColor = UIColor(red: 230/255, green: 6/255, blue: 6/255, alpha: 1)
cell?.statusxeLabel?.text = "Đang dừng"
cell?.speedLabel?.text = ""
}
else if list.statusxe == "expired"{
cell?.devnameLabel?.text = list.devname
cell?.addressLabel?.text = list.address
cell?.statusxeLabel?.textColor = UIColor.black
cell?.speedLabel?.textColor = UIColor.black
cell?.statusxeLabel.text = " "
cell?.speedLabel?.text = "hết hạn dịch vụ"
}
else if list.statusxe == "lost_gprs"{
cell?.devnameLabel?.text = list.devname
cell?.addressLabel?.text = list.address
cell?.statusxeLabel?.textColor = UIColor.red
cell?.statusxeLabel?.text = "Mất GPRS"
cell?.speedLabel?.text = ""
}
cell?.accessoryType = UITableViewCellAccessoryType.disclosureIndicator
return cell!
}
I have json request which show a list of cars. I used timer to refresh the request as well as the list every 10 seconds. The problem is that the data keep adding up to the array and make my application crashes. How can I clear the data before appending new data? What should I do?
let url = "http://api.vnetgps.com:8000/tracking"
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = "GET"
request.setValue(token , forHTTPHeaderField: "token")
request.setValue(username, forHTTPHeaderField: "username")
request.setValue("-1", forHTTPHeaderField: "devid")
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
if (error != nil ) {
print("Error")
}
else {
self.listdevices.removeAll()
if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any],
let items = json?["units"] as? [[String: Any]] {
for item in items {
var lat = item["latitude"] as? String
UserDefaults.standard.set(lat, forKey: "latitude")
var long = item["longitude"] as? String
UserDefaults.standard.set(long, forKey: "longitude")
// print("long", long)
var devid = item["devid"] as? String
UserDefaults.standard.set(devid, forKey: "devid")
var devname = item["devname"] as? String
UserDefaults.standard.set(devname, forKey: "devname")
var speed = item["speed"] as? String
UserDefaults.standard.set(speed, forKey: "speed")
var statustt = item["status"] as? String
UserDefaults.standard.set(statustt, forKey: "statusxe")
var drivername = item["drivername"] as? String
UserDefaults.standard.set(drivername, forKey: "drivername")
var address = item["address"] as? String
UserDefaults.standard.set(address, forKey: "address")
var direction = item["direction"] as? String
self.listdevices.append(Listdevices(statusxe: statustt! , speed: speed!, devid: devid!, devname: devname!, address: address!, latitude: lat!, longitude: long!, drivername: drivername!, direction: direction!))
// print("list",self.listdevices)
}
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
})
task.resume()
}
The problem is probably that you empty the array while it is being used by the tableview.
Instead use map to replace the old array contents with the new contents. That way you don't need to empty the array first.
Something like:
self.listDevices = items.map { Listdevices($0) }
And then implement an initialiser for ListDevices like this:
init(device: [String: Any]) { ... }
A couple of unsolicited code review comments:
All the writes to UserDefaults are pointless since each iteration just overwrites the previous one, so you should just remove the whole loop.
Make statusxe into an enum StatusXE
Replace the long conditional with a switch based on the new StatusXE enum
Be careful with naming. The variable list is not a list, so don't call it that, instead maybe call it device. The variable listdevices should just be called devices or if you insist on using the word list, it should be deviceList. Also remember proper camelCasing.
Avoid force unwrapping. Possibly use the nil coalescing operator to provide default values for device properties that are null.
Avoid unnecessarily duplicating code. In the long conditional, the first two lines of code are repeated in every case. Just move those two lines out of the conditional.
I think it could be due to the async nature of your code.
try avoid self.listdevices.append and use something like :
// declare listdevices with var instead of let
let temp = [Listdevices]()
for item in items {
...
temp.append(Listdevices(statusxe: statustt! , speed: speed!, devid: devid!, devname: devname!, address: address!, latitude: lat!, longitude: long!, drivername: drivername!, direction: direction!))
}
self.listdevices = temp

Swift - How To Error Check JSON File?

How would I be able to check whether or not the downloaded JSON content contains an error message rather than the expected content? I've tried to validate the URL but that cannot work due to how a false subdomain (location in this case) still returns an Error Message through the JSON content. I'd appreciate it if anybody could help me out. (Note: I want to check for an invalid location entered by the user and I'm using OpenWeatherMap API.)
func downloadData(completed: #escaping ()-> ()) {
print(url)
//UIApplication.shared.openURL(url as URL)
Alamofire.request(url).responseJSON(completionHandler: {
response in
let result = response.result
if let dict = result.value as? JSONStandard, let main = dict["main"] as? JSONStandard, let temp = main["temp"] as? Double, let weatherArray = dict["weather"] as? [JSONStandard], let weather = weatherArray[0]["main"] as? String, let name = dict["name"] as? String, let sys = dict["sys"] as? JSONStandard, let country = sys["country"] as? String, let dt = dict["dt"] as? Double {
self._temp = String(format: "%.0f °F", (1.8*(temp-273))+32)
self._weather = weather
self._location = "\(name), \(country)"
self._date = dt
}
completed()
})
}
Assuming the resulting JSON has different content when there is an error, check dict for the error content. Below is an example assuming there is a key named error. Adjust as needed based on what you really get when there is an error.
Alamofire.request(url).responseJSON(completionHandler: {
response in
let result = response.result
if let dict = result.value as? JSONStandard {
if let error = dict["error"] {
// parse the error details from the JSON and do what you want
} else if let main = dict["main"] as? JSONStandard, let temp = main["temp"] as? Double, let weatherArray = dict["weather"] as? [JSONStandard], let weather = weatherArray[0]["main"] as? String, let name = dict["name"] as? String, let sys = dict["sys"] as? JSONStandard, let country = sys["country"] as? String, let dt = dict["dt"] as? Double {
self._temp = String(format: "%.0f °F", (1.8*(temp-273))+32)
self._weather = weather
self._location = "\(name), \(country)"
self._date = dt
} else {
// Unexpected content, handle as needed
}
}
completed()
})
You should also provide a parameter to your downloadData completion handler so you can pass back an indication of success or failure so the caller can handle the result appropriately.