Swift JSON decoding url - json

I'm starting to hook up my frontend SwiftUI project to a backend and so I'm learning how to make API calls in Swift. I began by trying to use the code in this video as a workable example. However, I'm running into an error where the JSONDecoder is failing to decode the returned data.
What is going on here? My apologies for the blunt question--I'm new to backend development.
let url = "https://api.sunrise-sunset.org/json?date=2020-01-01&lat=-74.060&lng=40.7128&form"
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("Something Went Wrong...")
return
}
// data is available
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
} catch {
print("failed to convert \(error.localizedDescription)")
}
guard let json = result else {
return
}
print(json.status)
print(json.results.sunrise)
})
task.resume()
}
struct Response: Codable {
let results: MyResult
let status: String
}
struct MyResult: Codable {
let sunrise: String
let sunset: String
let solar_noon: String
let day_length: Int
let civil_twilight_begin: String
let nautical_twilight_begin: String
let nautical_twilight_end: String
let astronomical_twilight_begin: String
let astronomical_twilight_end: String
}
Note that I'm not following the video exactly. I took the getData function out of a
ViewController class because I didn't think it was relevant--I'm not dealing with any views at this point, and I don't want to be either. I also took the liberty to testing the code in a Swift Playground. I don't know whether that will influence things.

The main issue in your code is your day_length data type. You are trying to decode an Int while the data returned is a String. Besides that it is Swift naming convention to use camelCase to name your struct properties. All you need to do to match your json is to set your JSONDecoder object keyDecodingStrategy property to .convertFromSnakeCase. And when printing the decoding error you should print the error not its localizedDescription as already mentioned in comments:
struct Response: Codable {
let results: MyResult
let status: String
}
struct MyResult: Codable {
let sunrise: String
let sunset: String
let solarNoon: String
let dayLength: String
let civilTwilightBegin: String
let nauticalTwilightBegin: String
let nauticalTwilightEnd: String
let astronomicalTwilightBegin: String
let astronomicalTwilightEnd: String
}
Playground testing:
let json = """
{"results":{"sunrise":"12:00:01 AM","sunset":"12:00:01 AM","solar_noon":"9:20:25 AM","day_length":"00:00:00","civil_twilight_begin":"12:00:01 AM","civil_twilight_end":"12:00:01 AM","nautical_twilight_begin":"12:00:01 AM","nautical_twilight_end":"12:00:01 AM","astronomical_twilight_begin":"12:00:01 AM","astronomical_twilight_end":"12:00:01 AM"},"status":"OK"}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Response.self, from: Data(json.utf8))
print(result)
} catch {
print("Decoding error:", error)
}
This will print:
Response(results: MyResult(sunrise: "12:00:01 AM", sunset: "12:00:01 AM", solarNoon: "9:20:25 AM", dayLength: "00:00:00", civilTwilightBegin: "12:00:01 AM", nauticalTwilightBegin: "12:00:01 AM", nauticalTwilightEnd: "12:00:01 AM", astronomicalTwilightBegin: "12:00:01 AM", astronomicalTwilightEnd: "12:00:01 AM"), status: "OK")

Related

Nested Json data won't be decoded using Swift language?

I'm receiving response data from an API, when I'm trying to decode it by using json decoder, the nested json data won't be decoded because it returns null.
json data as follow:
{
"token": "string",
"details": {
"ID": "string",
"Name": "string",
"Message": null
}
}
Decoding model is:
struct response: Codable {
let token: String?
let usrData: userData?
}
struct userData:Codable{
let ID,Name,Message: String?
}
URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data, error == nil else {
completion(.failure(.custom(errorMessage: "Please check internet connection")))
return
}
guard let loginResponse = try? JSONDecoder().decode(response.self, from:data) else
{
completion(.failure(.invalidCredentials))
return
}
print(loginResponse.userData?.userID as Any) //returns nil
print(loginResponse.token) //token printed
guard let token = loginResponse.token else {
completion(.failure(.invalidCredentials))
return
}
completion(.success(token))
}.resume()
The token from the response will be successfully decoded, but the userData returns null.
Your model's property name and decoded json property name must be equal if your are not mapping , so change your struct with :
struct response: Codable {
let token: String?
let details: userDto?
}
That certainly can not work, first, in your response struct you have a let variable that says usrData that can not be identified as details. Second you write usrData: userDto what is userDto you clearly did a mistake or forgot to mention it. However, do it like that for example:
struct Response: Codable {
let token: String?
let details: UserData?
}
struct UserData: Codable {
let ID,Name,Message: String?
}
let filePath = Bundle.main.path(forResource:"test", ofType: "json")
let data = try Data(contentsOf: URL(fileURLWithPath: filePath!))
if let loginResponse = try? JSONDecoder().decode(Response.self, from: data) {
loginResponse
}
The example is not completely correct, because fileURLWithPath is deprecated but you should get the idea from it.
I also recommend following some basic roles, like writing Structs with an uppercase letter.

How would I print the title property in this returned JSON in Swift using URLSession and dataTask?

I am working with an api and getting back some strangely formatted JSON.
[
{
"title": "Wales' new £2bn space strategy hopes",
"url": "https://www.bbc.co.uk/news/uk-wales-60433763",
"source": "bbc"
},
{
"title": "Could Port Talbot become a centre of space tech? Video, 00:02:02Could Port Talbot become a centre of space tech?",
"url": "https://www.bbc.co.uk/news/uk-wales-60471170",
"source": "bbc"
},
]
As you can see, there is no object name I can latch on to. I've tried making a model like this
struct SpaceNewsModel: Identifiable, Codable {
var id = UUID()
let title: String
let url: String
let source: String
}
But once I get to using JSONDeocder() with the following code
let decoder = JSONDecoder()
if let safeData = data {
do {
let astroNews = try decoder.decode(SpaceNewsModel.self, from: safeData)
print(astroNews)
} catch {
print("DEBUG: Error getting news articles \(error.localizedDescription)")
}
}
I get the error DEBUG: Error getting news articles The data couldn’t be read because it isn’t in the correct format.
So how would you go about printing out each title, url, or source to the console? I've worked with JSON before and they are usually formatted differently.
I found a workaround for this. Removing the identifiable protocol lets me access the data. Like so
struct SpaceNewsModel: Codable {
let title: String
let url: String
let source: String
}
Then I can use decoder as normal
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
if error != nil {
print(error!)
} else {
let decoder = JSONDecoder()
if let safeData = data {
do {
let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
print(astroNews[0].title)
} catch {
print("DEBUG: Error getting news articles \(error)")
}
}
I did change let astroNews = try decoder.decode(SpaceNewsModel.self, from: safeData) to let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
Even with changing it to array type or not, as long as I had my SpaceNewsModel following the identifiable protocol, it would NOT work. It's a strange workaround, but it works for now.
in addition to
let astroNews = try decoder.decode([SpaceNewsModel].self, from: safeData)
use this common approach for your SpaceNewsModel. Having it Identifiable is very useful in SwiftUI.
struct SpaceNewsModel: Identifiable, Codable {
let id = UUID() // <-- here, a let
let title: String
let url: String
let source: String
enum CodingKeys: String, CodingKey { // <-- here
case title,url,source
}
}

Trouble Decoding JSON Data with Swift

Trying to get a little practice in decoding JSON data, and I am having a problem. I know the URL is valid, but for some reason my decoder keeps throwing an error. Below is my model struct, the JSON object I'm trying to decode, and my decoder.
Model Struct:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: String
let image: String
let phone: String
let date: String
let locationline1: String
let locationline2: String
}
struct EventResponse: Decodable {
let request: [Event]
}
JSON Response:
[
{
"id": 1,
"description": "Rebel Forces spotted on Hoth. Quell their rebellion for the Empire.",
"title": "Stop Rebel Forces",
"timestamp": "2015-06-18T17:02:02.614Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Battle_of_Hoth.jpg",
"date": "2015-06-18T23:30:00.000Z",
"locationline1": "Hoth",
"locationline2": "Anoat System"
},
{
"id": 2,
"description": "All force-sensitive members of the Empire must report to the Sith Academy on Korriban. Test your passion, attain power, to defeat your enemy on the way to becoming a Dark Lord of the Sith",
"title": "Sith Academy Orientation",
"timestamp": "2015-06-18T21:52:42.865Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Korriban_Valley_TOR.jpg",
"phone": "1 (800) 545-5334",
"date": "2015-09-27T15:00:00.000Z",
"locationline1": "Korriban",
"locationline2": "Horuset System"
},
{
"id": 3,
"description": "There is trade dispute between the Trade Federation and the outlying systems of the Galactic Republic, which has led to a blockade of the small planet of Naboo. You must smuggle supplies and rations to citizens of Naboo through the blockade of Trade Federation Battleships",
"title": "Run the Naboo Blockade",
"timestamp": "2015-06-26T03:50:54.161Z",
"image": "https://raw.githubusercontent.com/phunware-services/dev-interview-homework/master/Images/Blockade.jpg",
"phone": "1 (949) 172-0789",
"date": "2015-07-12T19:08:00.000Z",
"locationline1": "Naboo",
"locationline2": "Naboo System"
}
]
My Decoder:
func getEvents(completed: #escaping (Result<[Event], APError>) -> Void) {
guard let url = URL(string: eventURL) else {
completed(.failure(.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
if let _ = error {
completed(.failure(.unableToComplete))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completed(.failure(.invalidResponse))
return
}
guard let data = data else {
completed(.failure(.invalidData))
return
}
do {
let decoder = JSONDecoder()
let decodedResponse = try decoder.decode(EventResponse.self, from: data)
completed(.success(decodedResponse.request))
} catch {
completed(.failure(.invalidData))
}
}
task.resume()
}
I am sure the answer is pretty obvious to some but I have been beating my head against a wall. Thanks.
The EventResponse suggests that the JSON will be of the form:
{
"request": [...]
}
But that is obviously not what your JSON contains.
But you can replace:
let decodedResponse = try decoder.decode(EventResponse.self, from: data)
With:
let decodedResponse = try decoder.decode([Event].self, from: data)
And the EventResponse type is no longer needed.
FWIW, in the catch block, you are returning a .invalidData error. But the error that was thrown by decode(_:from:) includes meaning information about the parsing problem. I would suggest capturing/displaying that original error, as it will tell you exactly why it failed. Either print the error message in the catch block, or include the original error as an associated value in the invalidData error. But as it stands, you are discarding all of the useful information included in the error thrown by decode(_:from:).
Unrelated, but you might change Event to use URL and Date types:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: Date
let image: URL
let phone: String
let date: Date
let locationline1: String
let locationline2: String
}
And configure your date formatted to parse those dates for you:
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0) // not necessary because you have timezone in the date string, but useful if you ever use this formatter with `JSONEncoder`
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSX"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(formatter)
And if you want to have your Swift code follow camelCase naming conventions, even when the API does not, you can manually specify your coding keys:
struct Event: Identifiable, Decodable {
let id: Int
let description: String
let title: String
let timestamp: Date
let image: URL
let phone: String
let date: Date
let locationLine1: String
let locationLine2: String
enum CodingKeys: String, CodingKey {
case id, description, title, timestamp, image, phone, date
case locationLine1 = "locationline1"
case locationLine2 = "locationline2"
}
}

How to decode JSON response from API in SwiftUI to use in a view

I'm pretty new to developing apps and working with Xcode and Swift and trying to get a handle on how to decode JSON responses from an API. I'm also trying to follow MVVM practices.
I'm calling to an API that returns data structured like this:
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
"...": "..."
}
My understanding is I need a struct to decode this information into. This is what my struct looks like (it may be incorrect):
struct Example: Decodable {
let location: Coordinates
let date: String
let current_time: String
let sunset: String
let sunset: String
let ... :String
struct Coordinates: Decodable{
let latitude: Double
let longitude: Double
}
}
So I want to be able to call this api. I have the correct address to call it because the dashboard on the website I'm using is showing I've hit it. Could I get some guidance on how to call it and decode the response? Currently I'm doing something like this:
if let url = URL(string: "this is the api web address"){
URLSession.shared.dataTask(with: url){ (with: data, options [] ) as?
[String:String]{
print("(\json)" + "for debugging purposes")
}}
else{
print("error")
}
.resume()
Thanks for any and all help. Again, my goal is to call this function, hit the API, decode the JSON response and store it into a variable I can use in my views elsewhere. Thanks!
You are mixing JSONSerialization with Codable and URLSession. Forget about JSONSerialization. Try to focus on Codable protocol.
struct Example: Decodable {
let location: Coordinates
let date: String
let currentTime: String
let sunrise: String
let sunset: String
}
struct Coordinates: Decodable {
let latitude: Double
let longitude: Double
}
This is how you can decode your json data synchronously
extension JSONDecoder {
static let shared: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
}
extension Data {
func decodedObject<T: Decodable>() throws -> T {
try JSONDecoder.shared.decode(T.self, from: self)
}
}
Playground testing
let json = """
{
"location": {
"latitude": -27.4748,
"longitude": 153.017 },
"date": "2020-12-21",
"current_time": "21:55:42.198",
"sunrise": "04:49",
"sunset": "18:42",
}
"""
let data = Data(json.utf8)
do {
let example: Example = try data.decodedObject()
print(example)
} catch {
print(error)
}
And fetching your data asynchronously
extension URL {
func getResult<T: Decodable>(completion: #escaping (Result<T, Error>) -> Void) {
URLSession.shared.dataTask(with: self) { data, response, error in
guard let data = data, error == nil else {
completion(.failure(error!))
return
}
do {
completion(.success(try data.decodedObject()))
} catch {
completion(.failure(error))
}
}.resume()
}
}
let url = URL(string: "https://www.example.com/whatever")!
url.getResult { (result: Result<Example, Error>) in
switch result {
case let .success(example):
print(example)
case let .failure(error):
print(error)
}
}
If you need help with your SwiftUI project implementing MVVM feel free to open a new question.
Correct Method to decode a local JSON file in SwiftUI
do {
let path = Bundle.main.path(forResource: "filename", ofType: "json")
let url = URL(fileURLWithPath: path!)
let data = try Data(contentsOf: url)
let decodedresults = try JSONDecoder().decode(struct_name.self, from: data)
print(decodedresults)
} catch {
print(error)
}

json file is missing/ struct is wrong

I have been trying to get this code to work for like 6 hours. I get the error: "failed to convert The data couldn’t be read because it is missing." I don't know while the File is missing is there something wrong in my models(structs). Do I need to write a struct for very json dictionary? Currently I have only made those JSON dictionaries to a struct, which I actually need. The full JSON file can be found at https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00 . I want to be able to print the time of the sunrise, sunset and solar noon as well as the elevation of the sun at solar noon. It's currently 1 am and I am desperate. Good Night!
class ViewController: NSViewController {
#IBOutlet weak var sunriseField: NSTextField!
#IBOutlet weak var sunsetField: NSTextField!
#IBOutlet weak var daylengthField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
let url = "https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00"
getData(from: url)
// Do any additional setup after loading the view.
}
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("something went wrong")
return
}
var result: MyTime?
do {
result = try JSONDecoder().decode(MyTime.self, from: data)
}
catch {
print("failed to convert \(error.localizedDescription)")
}
guard let json = result else {
return
}
let sunrise1 = json.sunrise.time
DispatchQueue.main.async { [weak self] in
self?.sunriseField.stringValue = sunrise1
}
print(json)
})
task.resume()
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
struct MyData : Codable {
let location : Location
let meta : Meta
}
struct MyTime : Codable {
let solarnoon : Solarnoon
let sunset : Sunset
let sunrise : Sunrise
}
struct Location : Codable {
let height : String
let time : [MyTime]
let longitude : String
let latitude : String
}
struct Meta : Codable {
let licenseurl : String
}
struct Solarnoon : Codable {
let desc : String
let time : String
let elevation : String
}
struct Sunrise : Codable {
let desc : String
let time : String
}
struct Sunset : Codable {
let time : String
let desc : String
}
You don't really have a SwiftUI class, but that is a different question. I am going to work on fixing getData(). I have tried to comment it extensively, but let me know if you have any questions.
private func getData(from url: String) {
// Personally I like converting the string to a URL to unwrap it and make sure it is valid:
guard let url = URL(string: urlString) else {
print("Bad URL: \(urlString)")
return
}
let config = URLSessionConfiguration.default
// This will hold the request until you have internet
config.waitsForConnectivity = true
URLSession.shared.dataTask(with: url) { data, response, error in
// A check for a bad response
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
print("Bad Server Response")
return
}
if let data = data {
// You can print(data) here that will shown you the number of bytes returned for debugging.
//This work needs to be done on the main thread:
DispatchQueue.main.async {
let decoder = JSONDecoder()
if let json = try? decoder.decode(MetDecoder.self, from: data){
print(json)
//At this point, you have your data in a struct
self.sunriseTime = json.dailyData?.solarData?.first?.sunrise?.time
}
}
}
}
.resume()
}
With regard to your structs, you only need them for the data you are trying to parse. If you don't need it, don't worry about it. I would make this a separate class named MetDecoder or something that makes sense to you and indicates the decoder for your JSON. You will also note that I changed the names of some of the variables. You can do that so long as you use a CodingKeys enum to translate your JSON to your struct as in the case of dailyData = "location", etc. This is ugly JSON, and I am not sure why the Met decided everything should be a string, but this decoder is tested and it works:
import Foundation
// MARK: - MetDecoder
struct MetDecoder: Codable {
let dailyData: DailyData?
enum CodingKeys: String, CodingKey {
case dailyData = "location"
}
}
// MARK: - Location
struct DailyData: Codable {
let solarData: [SolarData]?
enum CodingKeys: String, CodingKey {
case solarData = "time"
}
}
// MARK: - Time
struct SolarData: Codable {
let sunrise, sunset: RiseSet?
let solarnoon: Position?
let date: String?
enum CodingKeys: String, CodingKey {
case sunrise, sunset, solarnoon, date
}
}
// MARK: - HighMoon
struct Position: Codable {
let time: String?
let desc, elevation, azimuth: String?
}
// MARK: - Moonrise
struct RiseSet: Codable {
let time: String?
let desc: String?
}
You should see what the National Weather Service does to us in the US to get the JSON. Lastly, when working on JSON I find the following pages VERY helpful:
JSON Formatter & Validator which will help you parse out the wall of text that gets returned in a browser, and
quicktype which will parse JSON into a programming language like Swift. I will warn you that the parsing can give some very ugly structs in Swift, but it gives you a nice start. I used both sites for this answer.
Apple's new framework, Combine, helps to simplify the code needed for async fetch requests. I have used the MetDecoder in #Yrb's response above (you can accept his answer) and altered the getData() function. Just make sure you import Combine at the top.
import Combine
var sunriseTime: String?
var sunsetTime: String?
var solarNoonTime: String?
var solarNoonElevation: String?
func getData() {
let url = URL(string: "https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-12-22&offset=-05:00")!
URLSession.shared.dataTaskPublisher(for: url)
// fetch on background thread
.subscribe(on: DispatchQueue.global(qos: .background))
// recieve response on main thread
.receive(on: DispatchQueue.main)
// ensure there is data
.tryMap { (data, response) in
guard
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
// decode JSON data to MetDecoder
.decode(type: MetDecoder.self, decoder: JSONDecoder())
// Handle results
.sink { (result) in
// will return success or failure
print("completion: \(result)")
} receiveValue: { (value) in
// if success, will return MetDecoder
// here you can update your view
print("value: \(value)")
if let solarData = value.dailyData?.solarData?.first {
self.sunriseTime = solarData.sunrise?.time
self.sunsetTime = solarData.sunset?.time
self.solarNoonTime = solarData.solarnoon?.time
self.solarNoonElevation = solarData.solarnoon?.elevation
}
}
// After recieving response, the URLSession is no longer needed & we can cancel the publisher
.cancel()
}
from the json data (2nd entry), looks like you need at least:
struct MyTime : Codable {
let solarnoon : Solarnoon?
let sunset : Sunset?
let sunrise : Sunrise?
}
and you need:
var result: MyData?
do {
result = try JSONDecoder().decode(MyData.self, from: data)
}
catch {
print("----> error failed to convert \(error)")
}