Swift 4 Not decoding JSON optional properly - json

Swift noob here.
I'm trying to follow the App Development With Swift book and am running into trouble with decoding JSON data from the NASA API as given in the examples. Here's the code I'm trying to use:
struct PhotoInfo: Codable {
var title: String
var description: String
var url: URL
var copyright: String?
enum CodingKeys: String, CodingKey {
case title
case description = "explanation"
case url
case copyright
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
self.title = try valueContainer.decode(String.self, forKey: CodingKeys.title)
self.description = try valueContainer.decode(String.self, forKey: CodingKeys.description)
self.url = try valueContainer.decode(URL.self, forKey: CodingKeys.url)
self.copyright = try valueContainer.decode(String.self, forKey: CodingKeys.copyright)
}
}
func fetchPhotoInfo(completion: #escaping (PhotoInfo?) -> Void) {
let baseURL = URL(string: "https:/
let query: [String: String] = [
"api_key": "yN3**0scRWo12gCa25TWBcfp3rcuAnoeqwbpvLPn",
"date": "2011-07-13"
]
let url = baseURL.withQueries(query)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let photoInfo = try? jsonDecoder.decode(PhotoInfo.self, from: data) {
print(data)
completion(photoInfo)
} else {
print("Either no data was returned, or data was not properly decoded.")
completion(nil)
}
}
task.resume()
}
When I remove the copyright code from the PhotoInfo struct, it decodes the JSON and prints the data (line 36). Otherwise, it doesn't deserialize it. Is there a way I can troubleshoot why this is happening? Does it have something to do with the optional?

If copyright is optional , then you can make use of decodeIfPresent.
self.copyright = try valueContainer.decodeIfPresent(String.self, forKey: CodingKeys.copyright)

EDIT: Updated per #matt's comments.
technerd's answer is probably best, but I faced the same problem and dealt with it by changing try to try?. That's how it was presented in previous exercises, anyway. However, as #matt points out, this doesn't deal with the possibility that the copyright information isn't simply missing, but in fact something other than what was expected. Using try? in tandem with an if let statement provides at least basic feedback.

Related

Swift JSON Search Date format by Year-month?

I am using Eodhistoricaldata.com's API to get the monthly values of issues.
https://eodhistoricaldata.com/api/eod/VTSMX?from=2017-09-01&api_token=xxxxxx&period=m&fmt=json
And even though it is monthly data they are assigning the first trade date to the results. -01, -02, -03, etc
This means I can not use a generic date of -01. So YYYY-MM-01 does not work.
So I either have to change all the dates to -01 or search by just the year and month like "2017-10"
What is the best way to accomplish this using Swift 4 and SwiftyJSON.
Thanks.
Here is their data.
[{"date":"2017-09-01","open":"61.9300","high":63.03,"low":"61.4400","close":63.03,"adjusted_close":61.6402,"volume":0},
{"date":"2017-10-02","open":"63.3400","high":"64.5300","low":"63.3400","close":64.39,"adjusted_close":62.9703,"volume":0},
{"date":"2017-11-01","open":"64.4400","high":66.35,"low":"64.0600","close":66.35,"adjusted_close":64.8872,"volume":0},
{"date":"2017-12-01","open":"66.2100","high":"67.3500","low":"65.7700","close":66.7,"adjusted_close":65.5322,"volume":0},
{"date":"2018-01-02","open":"67.2500","high":"71.4800","low":"67.2500","close":70.24,"adjusted_close":69.0102,"volume":0},
{"date":"2018-02-01","open":"70.2400","high":"70.2400","low":"64.4000","close":67.63,"adjusted_close":66.4458,"volume":0},
....
{"date":"2018-12-03","open":"69.5700","high":"69.5700","low":"58.1700","close":62.08,"adjusted_close":62.08,"volume":0}]
Drop SwiftyJSON and use Decodable to parse the JSON into a struct. Parsing dates is highly customizable. You can add your own logic which extracts year and month from the date string and create a Date instance.
struct HistoricalData: Decodable {
let date: Date
let open, low, high : String
let close, adjustedClose, volume : Double
}
...
let jsonString = """
[{"date":"2017-09-01","open":"61.9300","high":"63.03","low":"61.4400","close":63.03,"adjusted_close":61.6402,"volume":0},
{"date":"2017-10-02","open":"63.3400","high":"64.5300","low":"63.3400","close":64.39,"adjusted_close":62.9703,"volume":0},
{"date":"2017-11-01","open":"64.4400","high":"66.35","low":"64.0600","close":66.35,"adjusted_close":64.8872,"volume":0},
{"date":"2017-12-01","open":"66.2100","high":"67.3500","low":"65.7700","close":66.7,"adjusted_close":65.5322,"volume":0},
{"date":"2018-01-02","open":"67.2500","high":"71.4800","low":"67.2500","close":70.24,"adjusted_close":69.0102,"volume":0},
{"date":"2018-02-01","open":"70.2400","high":"70.2400","low":"64.4000","close":67.63,"adjusted_close":66.4458,"volume":0},
{"date":"2018-12-03","open":"69.5700","high":"69.5700","low":"58.1700","close":62.08,"adjusted_close":62.08,"volume":0}]
"""
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .custom { decoder -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
let components = dateStr.components(separatedBy: "-")
guard components.count > 2, let year = Int(components[0]), let month = Int(components[1]) else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date string") }
return Calendar.current.date(from: DateComponents(year: year, month: month))!
}
do {
let result = try decoder.decode([HistoricalData].self, from: data)
print(result)
} catch { print(error) }
Alternatively you can decode the string to format yyyy-MM however you have to write an initializer and add CodingKeys
struct HistoricalData: Decodable {
let date: String
let open, low, high : String
let close, adjustedClose, volume : Double
private enum CodingKeys : String, CodingKey {
case date, open, low, high, close, adjustedClose, volume
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .date)
guard let secondDashRange = dateString.range(of: "-", options: .backwards) else {
throw DecodingError.dataCorruptedError(forKey: .date, in: container, debugDescription: "Invalid date string")
}
date = String(dateString[..<secondDashRange.lowerBound])
open = try container.decode(String.self, forKey: .open)
low = try container.decode(String.self, forKey: .low)
high = try container.decode(String.self, forKey: .high)
close = try container.decode(Double.self, forKey: .close)
adjustedClose = try container.decode(Double.self, forKey: .adjustedClose)
volume = try container.decode(Double.self, forKey: .volume)
}
}
let data = Data(jsonString.utf8)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let result = try decoder.decode([HistoricalData].self, from: data)
print(result)
} catch { print(error) }
I was able to work with the API company to overcome this issue with eodhistorical data.
They now have written a special API to get back just the last monthly data - so no more need to write crazy Json code.
There is now a special request for the data using "period = eom"
jsonUrl = "https://eodhistoricaldata.com/api/eod/(symbol).US?from=(beg)&to=(end)&api_token=(EOD_KEY)&period=eom&fmt=json"
this really made live much better. (After wasting house trying to overcome their data.)

Decoding JSON in Swift 4

I am working through the Apple App Development Guide and this is the code I am working with right now...
struct CategoryInfo: Codable {
var category: String
var description: String
var logo: String
var mobileCategoryName: String
enum Keys: String, CodingKey {
case category
case description = "descr"
case logo
case mobileCategoryName = "mobileCatName"
}
init(from decoder: Decoder) throws {
let valueContainer = try decoder.container(keyedBy: Keys.self)
self.category = try valueContainer.decode(String.self, forKey: Keys.category)
self.description = try valueContainer.decode(String.self, forKey: Keys.description)
self.logo = try valueContainer.decode(String.self, forKey: Keys.logo)
self.mobileCategoryName = try valueContainer.decode(String.self, forKey: Keys.mobileCategoryName)
}
}
override func viewDidLoad() {
super.viewDidLoad()
let categories = Industry_TableViewController()
categories.fetchCategoryInfo { (category) in
if let category = category {
print(category)
}
}
}
func fetchCategoryInfo(completion: #escaping(CategoryInfo?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let category = try? jsonDecoder.decode(CategoryInfo.self, from: data) {
completion(category)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}
it works fine when my returned JSON is in the following format...
{"category":"Excavators","descr":"Compact, Mid-Sized, Large, Wheeled, Tracked...","logo":"excavators","mobileCatName":"Excavators"}
My struct is created and all the variables are populated correctly. But the API doesn't bring back one category at a time it brings back multiple like so...
[{"category":"Aerial Lifts","descr":"Aerial Lifts, Man Lifts, Scissor Lifts...","logo":"aeriallifts","mobileCatName":"Aerial Lifts"},{"category":"Aggregate Equipment","descr":"Crushing, Screening, Conveyors, Feeders and Stackers...","logo":"aggregateequipment","mobileCatName":"Aggregate"},{"category":"Agricultural Equipment","descr":"Tractors, Harvesters, Combines, Tillers...","logo":"agricultural","mobileCatName":"Agricultural"}]
And I am running into a wall trying to figure out how to get this decoded properly. I've gone down so many routes I don't even know what to search for any more. Can anyone help or point me in a direction.
You need to modify your function to parse an array of categories instead of a single one. You just need to pass the Array<CategoryInfo> metatype to the decode function and modify the function signature such that the completion handler also returns an array.
func fetchCategoryInfo(completion: #escaping ([CategoryInfo]?) -> Void) {
let url = URL(string: "XXXXX")!
let task = URLSession.shared.dataTask(with: url) {
(data, response, error) in
let jsonDecoder = JSONDecoder()
if let data = data,
let categories = try? jsonDecoder.decode([CategoryInfo].self, from: data) {
completion(categories)
} else {
print("Nothing reutrned or Not decoded")
completion(nil)
}
}
task.resume()
}
try? jsonDecoder.decode([CategoryInfo].self, from: data)

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

Umlaut in URL fails Codable (Swift) - what to do?

Using Swift-4.1, Xcode-9.3.1, iOS-11.3.1
I use the Codable protocol to decode a JSON-file. Everything works, except until the moment where I have an Internationalised Domain-Name (in this case, with a German Umlaut "ä") in a URL (example: http://www.rhätische-zeitung.ch).
This leads to a decoder-error inside the following code:
func loadJSON(url: URL) -> Media? {
do {
let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
let media = try decoder.decode(Media.self, from: data)
return media
} catch {
print("error:\(error)")
}
return nil
}
The error-message is:
The Codable protocol does not seem to be able to decode this URL form
my JSON-file into the needed Struct.
Here is the Struct:
struct Media: Codable {
var publisher: [MediaPublisher]
}
struct MediaPublisher: Codable {
var title: String?
var homepage_url: URL?
}
And here is the JSON-excerpt:
{
"publisher": [
{
"title" : "Rhätische-Zeitung",
"homepage_url" : "http://www.rhätische-zeitung.ch",
}
]
}
Since the JSON-file is coming from the outside, I have no control over the content. And therefore, replacing the URL inside the JSON is not an option !
(Therefore, I cannot replace the URL inside the JSON to an accepted Internationalized Form suche as: www.xn--rhtische-zeitung-wnb.ch) !!
I know that there are techniques to place a custom initialiser into the Struct-definition (see my trials below...) - but since new to Codable, I don't know how to do that for this current URL-Umlaut problem. The custom-initialiser I placed below does return nil for the URL at question. What do I need to change ??
Or is there another way of making this JSON-decoding of an URL with Umlaut work ??
Here is the Struct, this time with a custom initialiser:
(at least with this, I can get rid of the error-message above... But the URL is now nil it seems and that is not what I want either)
struct Media: Codable {
var publisher: [MediaPublisher]
}
struct MediaPublisher: Codable {
var title: String?
var homepage_url: URL?
// default initializer
init(title: String?, homepage_url: URL?) {
self.title = title
self.homepage_url = homepage_url
}
// custom initializer
init(from decoder: Decoder) throws {
let map = try decoder.container(keyedBy: CodingKeys.self)
self.title = try? map.decode(String.self, forKey: .title)
self.homepage_url = try? map.decode(URL.self, forKey: .homepage_url)
}
private enum CodingKeys: CodingKey {
case title
case homepage_url
}
}
I just stumbled over the same problem. The solution is actually fairly simple and works well in my brief testing.
The idea is to not let the decoder decode the value as an URL, because it expects the string behind it to be in of a certain format. What we can do to circumvent this is to decode the value as a string directly and convert that manually into a URL.
I wrote a little extension that does that job for me:
extension KeyedDecodingContainer {
/// Decodes the string at the given key as a URL. This allows for special characters like umlauts to be decoded correctly.
func decodeSanitizedURL(forKey key: KeyedDecodingContainer<K>.Key) throws -> URL {
let urlString = try self.decode(String.self, forKey: key)
// Sanitize string and attempt to convert it into a valid url
if let urlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let url = URL(string: urlString) {
return url
}
// Throw an error as the URL could not be decoded
else {
throw DecodingError.dataCorruptedError(forKey: key, in: self, debugDescription: "Could not decode \(urlString)")
}
}
}
This allows for a streamlined use in the init method.
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.url = try values.decodeSanitizedURL(forKey: .url)
}
Hope that helps, even if the question is a little bit older.