json file is missing/ struct is wrong - json

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

Related

SwiftUI decoding JSON from API

I know there are already some articles regarding this issue, but I could not find anything related to my JSON.
This is how my JSON likes like:
{
"message": {
"affenpinscher": [],
"african": [],
"airedale": [],
"akita": [],
"appenzeller": [],
"australian": [
"shepherd"
],
"basenji": []
},
"status: "succes"
}
So, if I understand it correctly it is dictionary because it starts with {, but what are the things inside the "message"?
This is my Dog.swift class where I am re-writing the JSON, but I am not sure if it is correct:
class Dog: Decodable, Identifiable {
var message: Message?
var status: String?
}
struct Message: Decodable {
var affenpinscher: [String:[String]]?
var african: [String]?
var airedale: [String]?
var akita: [String]?
var appenzeller: [String]?
var australian: [String]?
var basenji: [String]?
}
As you can see in the first value I was trying to play with data types, but no success.
I am decoding and parsing JSON here:
class ContentModel: ObservableObject {
#Published var dogs = Message()
init() {
getDogs()
}
func getDogs(){
// Create URL
let urlString = Constants.apiUrl
let url = URL(string: urlString)
if let url = url {
// Create URL request
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10)
request.httpMethod = "GET"
// Get URLSession
let session = URLSession.shared
// Create Data Task
let dataTask = session.dataTask(with: request) { (data, response, error) in
// Check that there is not an error
if error == nil {
do {
// Parse JSON
let decoder = JSONDecoder()
let result = try decoder.decode(Dog.self, from: data!)
print(result)
// Assign result to the dogs property
DispatchQueue.main.async {
self.dogs = result.message!
}
} catch {
print(error)
}
}
}
// Start the Data Task
dataTask.resume()
}
}
}
And here I would love to iterate through it eventually, which I also have no idea how to do it:
struct ContentView: View {
#EnvironmentObject var model: ContentModel
var body: some View {
NavigationView {
ScrollView {
LazyVStack {
if model.dogs != nil {
// ForEach(Array(model.dogs.keys), id: \.self) { d in
// Text(d)
// }
}
}
.navigationTitle("All Dogs")
}
}
}
}
What can I try next to resolve this?
First of all don't use classes for a JSON model and to conform to Identifiable you have to add an id property and CodingKeys if there is no key id in the JSON.
My suggestion is to map the unhandy [String: [String]] dictionary to an array of an extra struct
I renamed Dog as Response and named the extra struct Dog
struct Dog {
let name : String
let types : [String]
}
struct Response: Decodable, Identifiable {
private enum CodingKeys: String, CodingKey { case message, status }
let id = UUID()
let dogs: [Dog]
let status: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String.self, forKey: .status)
let message = try container.decode([String:[String]].self, forKey: .message)
dogs = message.map(Dog.init).sorted{$0.name < $1.name}
}
}
In the model declare
#Published var dogs = [Dog]()
and decode
let result = try decoder.decode(Response.self, from: data!)
print(result)
// Assign result to the dogs property
DispatchQueue.main.async {
self.dogs = result.dogs
}
The dogs array can be displayed seamlessly in a List
PS: Actually appenzeller is supposed to be
"appenzeller": ["sennenhund"],
or correctly in English
"appenzell": ["mountain dog"],
😉😉😉
Using the native Swift approach that #vadian answered is a much lighter weight solution, but if you work with JSON often I'd recommend using SwiftyJSON.
You can parse the URL data task response into a Swifty json object like so:
import SwiftyJSON
guard let data = data, let json = try? JSON(data: data) else {
return
}
// Make sure the json fetch was successful
if json["status"].stringValue != "success" {
return
}
Then you can access the message object safely without the verbosity of using Decodable. Here the message is parsed into an array of dog structs:
struct Dog {
let name : String
let types : [String]
}
var dogs: [Dog] = []
/// Load the docs into an array
for (name, typesJson) in json["message"].dictionaryValue {
dogs.append(Dog(name: name, types: typesJson.arrayValue.map { $0.stringValue }))
}
print("dogs", dogs)

How do I load a remote JSON file in Swift?

I'm pretty much a complete beginner to Swift. I've played around with SwiftUI a bit, but that's about it. Needless to say, I have no idea how classes, structs, protocols, and everything else works in Swift.
I'm currently trying to figure out how to load a JSON file into Swift, and I cannot for the life of me get it to work. I would think that such a thing would be fairly rudimentary for such a modern language, but apparently not. After trying multiple tutorials and examples, I've come up with this messy code:
public class JSONReader {
struct DatabaseObject: Decodable {
let name: String
let books: AnyObject
let memoryVerses: AnyObject
}
private func parse(jsonData: Data) {
do {
let decodedData = try JSONDecoder().decode(DatabaseObject.self, from: jsonData)
print(decodedData)
} catch {
print("decode error")
}
}
private func loadJson(fromURLString urlString: String,
completion: #escaping (Result<Data, Error>) -> Void) {
if let url = URL(string: urlString) {
let urlSession = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
if let data = data {
completion(.success(data))
}
}
urlSession.resume()
}
}
init() {
loadJson(fromURLString: "Redacted for privacy") { result in
switch result {
case .success(let data):
self.parse(jsonData: data)
case .failure(let error):
print(error)
}
}
}
}
I keep getting the buildtime error Type 'JSONReader.DatabaseObject' does not conform to protocol 'Decodable'
Any help, pointers, or tips would be greatly appreciated!
The type AnyObject is not decodable by swift , The books property you are trying to decode and memoryVerse are not decodable , If you want to Decode this you can define their types separately as codable structs like so
struct Book : Codable{
let property : String
let otherProperty : String
}
struct Memory : Codable{
let property : String
let otherProperty : String
}
struct DatabaseObject: Decodable {
let name: String
let books: Book
let memoryVerses:Memory
}

Need help targeting a nested json object SwiftUI *Question clarified*

Im currently hitting a wall as I'm trying to display information from an array within a nested json object. I can't understand where I am going wrong, so any help will be greatly appreciated. **The current error I am receiving is : Fatal error: Index out of range
I believe my problem may be in regards to how I am targeting the information. I have outlined using "!! ... !!" the key areas where I believe the error is being made.
I am confused because of how the json object is nested. If I'm correct, the heiarchy for my desired target is: object(main object) -> object(DATA) -> array(newReleases) -> String(prId). With that being said Im under the impression my self.posts = results.data.newReleases would then be targeting the newReleases array directly(which I want), for it to then be printed in the contentView Text(networkManager.posts[0].prID)
Content View where the list is declared:
struct ContentView: View {
#ObservedObject var networkManager = NetworkManager()
var body: some View {
NavigationView {
List{
Text(networkManager.posts[0].prID)
}
.navigationTitle("Json Test")
}
.onAppear{
self.networkManager.fetchData()
}
}
}
Heres a photo of the JSON Data formatted.
JSON Data
This is how I have the data defined in my app:
// MARK: - Results
struct Results: Codable {
let data: DATAClass
enum CodingKeys: String, CodingKey {
case data = "DATA"
}
}
// MARK: - DATAClass
struct DATAClass: Codable {
let newReleases, exclusives, preorders, backIssues: [BackIssue]
}
// MARK: - BackIssue
struct BackIssue: Codable {
let totalcount: String
let sectionName: SectionName
let sectionLink, prID, prParentid, prTtle: String
let prPrice, prLprice, prSimg, prBimg: String
enum CodingKeys: String, CodingKey {
case totalcount
case sectionName = "section_name"
case prID = "pr_id"
case prTtle = "pr_ttle"
case prPrice = "pr_price"
}
}
}
enum SectionName: String, Codable {
case featuredNewReleases = "FEATURED_NEW_RELEASES"
case recommendedBackIssues = "RECOMMENDED_BACK_ISSUES"
case recommendedPreOrders = "RECOMMENDED_PRE_ORDERS"
}
Heres where I am attempting to decode the json object:
#Published var posts = !! [BackIssue]() !!
func fetchData() {
if let url = URL(string:" https://www.midtowncomics.com/wcfmt/services/product.svc/load-featured-sections?apiKey=&mtUser=&mtPass=&sh_id=76367&pgn=home&app_id") {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
!! self.posts = results.data.newReleases !!
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
Never get an item by hard-coded index in the rendering area of an SwiftUI view. If the array is empty the code crashes reliably.
Get the item safely by replacing
Text(networkManager.posts[0].prID)
with
Text(networkManager.posts.first?.prID ?? "n/a")
Better is a ForEach loop which is skipped if the array is empty.

How to extract the result of json with Codable

I'm using Codable for the first time and want to output the json result of Google Places details as a label.
However, when I print it, the console says "The data could n’t be read because it isn’t in the correct format.”.
I can't solve it by myself, so please tell me how to write it correctly.
Thanks.
The result of json
{
"html_attributions": [],
"result": {
"formatted_phone_number": "XXXX-XXX-XXX",
"website": "https://www.xxxxx.com/xxxxxx/"
},
"status": "OK"
}
Detail.swift
import Foundation
struct Details : Codable {
var formatted_phone_number : String!
var website : String!
}
ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
fetchDetailData {(details) in
for detail in details{
print(detail.website)
}
}
}
func fetchDetailData(completionHandler: #escaping ([Details]) -> Void){
let url = URL(string: "https://maps.googleapis.com/maps/api/place/details/json?place_id=\(place_id)&fields=formatted_phone_number,website&key=\(apikey)")!
let task = URLSession.shared.dataTask(with: url){
(data,respose, error)in
guard let data = data else{ return }
do {
let detailsData = try JSONDecoder().decode([Details].self, from: data)
completionHandler(detailsData)
}
catch{
let error = error
print(error.localizedDescription)
}
}.resume()
}
One of the issues there is that result is a dictionary not an array. You need also to decode the root structure to extract the result from it. Note that you can also change the website type from String to URL:
struct Root: Codable {
let htmlAttributions: [String] // make sure to define the proper type in case the collection is not empty
let result: Result
let status: String
}
struct Result: Codable {
let formattedPhoneNumber: String
let website: URL
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Root.self, from: data).result
print(result)
} catch {
print(error)
}
This will print
Result(formattedPhoneNumber: "XXXX-XXX-XXX", website: https://www.xxxxx.com/xxxxxx/)
could you try this;
struct Place {
let result: Details?
}
struct Details: Codable {
let phoneNumber: String?
let website: String?
enum CodingKeys: String, CodingKey {
case website
case phoneNumber = "formatted_phone_number"
}
}
and parse Place.self
you will also need to change "#escaping ([Details])" to "#escaping (Place)"

Syntax for accessing struct property with enum type in JSON model

I am trying to access a url string contained within some JSON data.
The string is contained within the "urls" array with type "detail" as can be seen below.
JSON DATA
I used quicktype to construct my model as below:
struct Response: Codable {
let data: DataClass
}
struct DataClass: Codable {
let results: [Result]
}
struct Result: Codable {
let name: String
let description: String
let thumbnail: Thumbnail
let urls: [URLElement]
}
struct Thumbnail: Codable {
let path: String
let thumbnailExtension: Extension
enum CodingKeys: String, CodingKey {
case path
case thumbnailExtension = "extension"
}
}
enum Extension: String, Codable {
case jpg = "jpg"
}
struct URLElement: Codable {
let type: URLType
let url: String
}
enum URLType: String, Codable {
case comiclink = "comiclink"
case detail = "detail"
case wiki = "wiki"
}
I have tried accessing it by declaring it like so...
var urlelement: URLElement!
override func viewDidLoad() {
super.viewDidLoad()
let detailurl = urlelement.url
print(detailurl)
... but it always returns an empty string. Any suggestions will be most welcome. Thanks!
First Download the JSON then user JSONDecoder
let url = URL(string: "your url")!
URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("error \(error.localizedDescription)")
return
}
guard let data = data else { return }
do {
let response = try JSONDecoder().decode(Response.self, from: data)
// use this
response?.data.results.forEach({ (rsl) in
rsl.urls.forEach({ (element) in
print(element.type, element.url)
})
})
// or this one
for rsl in response!.data.results {
for element in rsl.urls {
print(element.type, element.url)
}
}
} catch let error {
print("error while decoding the json \(error.localizedDescription)")
}
}.resume()