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
}
Related
i'm trying to do with API Call. I got error every time trying to do API Call.
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array but found a dictionary instead.", underlyingError: nil))
this is what i see when simulate my code in console.
Here is json format i try to call in my app. Click
My Model
struct Article: Codable {
let author: String
let title, articleDescription: String
let url: String
let urlToImage: String
let publishedAt: Date
let content: String?
enum CodingKeys: String, CodingKey {
case author, title
case articleDescription = "description"
case url, urlToImage, publishedAt, content
}
}
and This is my API Call function.
import UIKit
class ViewController: UIViewController {
var article = [Article]()
override func viewDidLoad() {
super.viewDidLoad()
jsonParse {
print("success")
}
view.backgroundColor = .red
}
func jsonParse(completed: #escaping () -> ()) {
let url = URL(string: "https://newsapi.org/v2/top-headlines?country=tr&apiKey=1ea9c2d2fbe74278883a8dc0c9eb912f")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
if error != nil {
print(error?.localizedDescription as Any)
}else {
do {
let result = try JSONDecoder().decode([Article].self, from: data!)
DispatchQueue.main.async {
print(data as Any)
print("success")
self.jsonParse {
print("success")
}
}
}catch {
print(error.localizedDescription)
}
}
}
task.resume()
}
}
Can you help me about my problem, thank you.
This is a very common mistake: You ignore the root object, a dictionary. With Decodable it's mandatory to decode the JSON from the top.
Add this struct
struct Response: Decodable {
let status: String
let totalResults: Int
let articles: [Article]
}
and decode
let result = try JSONDecoder().decode(Response.self, from: data!)
And never print(error.localizedDescription) in a Codable catch block. Always write this
} catch {
print(error)
}
I have looked at the other answers for similar questions to this but can not find one that explains why this is coming up when requesting data from API. I am mostly confused because the error occurs when I copy the path from the JSON. After reading the error code and the documentation, I think it is something to do with an expected dictionary but found an array instead. I don't understand how to fix this when the data is coming from an API request.
Here is the JSON data that I am aiming to pull from:
Here is a picture of the error:
Here the code I have so far:
import Foundation
struct VaccineManager {
let vaccineURL = "https://coronavirus.data.gov.uk/api/v1/data?filters=areaType=overview&structure=%7B%22areaType%22:%22areaType%22,%22areaName%22:%22areaName%22,%22areaCode%22:%22areaCode%22,%22date%22:%22date%22,%22newPeopleVaccinatedFirstDoseByPublishDate%22:%22newPeopleVaccinatedFirstDoseByPublishDate%22,%22newPeopleVaccinatedSecondDoseByPublishDate%22:%22newPeopleVaccinatedSecondDoseByPublishDate%22,%22cumPeopleVaccinatedFirstDoseByPublishDate%22:%22cumPeopleVaccinatedFirstDoseByPublishDate%22,%22cumPeopleVaccinatedSecondDoseByPublishDate%22:%22cumPeopleVaccinatedSecondDoseByPublishDate%22%7D&format=json"
func performRequest(vaccineURL: String){
if let url = URL(string: vaccineURL) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil {
print(error!)
return
}
if let safeData = data {
self.parseJASON(vaccineData: safeData)
}
}
task.resume()
}
}
func parseJASON(vaccineData: Data) {
let decoder = JSONDecoder()
do{
let decodedData = try decoder.decode(VaccineData.self, from: vaccineData)
print(decodedData.data[0].cumPeopleVaccinatedFirstDoseByPublishDate)
} catch {
print(error)
}
}
And this is the code from the other file I have to for the data:
import Foundation
struct VaccineData: Decodable {
let data: VaccineDataObject
}
struct VaccineDataObject: Decodable {
let cumPeopleVaccinatedFirstDoseByPublishDate: Int
let date: String
}
I hope that all makes sense. Also, please tell me if I am being too detailed.
Your struct definition is incorrect. It says there's a single value in data, but you have an array:
struct VaccineData: Decodable {
let data: VaccineDataObject
}
Should be:
struct VaccineData: Decodable {
let data: [VaccineDataObject]
}
You may find QuickType helpful here. It will take JSON and write decoding structs for you automatically. (Though it looks like you are pulling out such a small part, that it may be easier to hand-write as you have.)
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)"
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)")
}
I am trying to use the open weathermap.org api to return the "main" key in JSON. For some reason I keep getting caught up in my error around "failed to convert". I'm not exactly sure why I am failing to convert the JSON anyone have any ideas? Thank you.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let url = "https://api.openweathermap.org/data/2.5/weather?zip=10514&appid=#####################"
getData(from: url)
}
struct Response: Codable {
let weather: MyResult
}
struct MyResult: Codable {
let main: String
}
Below is the function being called to change access the JSON.
private func getData(from url:String) {
URLSession.shared.dataTask(with: URL(string:url)!, completionHandler: {
data, response, error in
guard let data = data, error == nil else {
print("Something went wrong")
return
}
// have data
var result: Response?
do {
result = try JSONDecoder().decode(Response.self, from: data)
} catch {
print("failed to convert")
}
guard let json = result else {
return
}
print(json.weather.main)
}).resume()
}
EDIT: This is what the json pulls up. Am I creating the structs incorrectly? if so how should I do it properly? thank you
{"coord":{"lon":-73.73,"lat":41.2},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03n"}],"base":"stations","main":{"temp":291.67,"feels_like":288.73,"temp_min":290.93,"temp_max":292.59,"pressure":1024,"humidity":72},"visibility":10000,"wind":{"speed":5.7,"deg":130},"clouds":{"all":40},"dt":1594956028,"sys":{"type":1,"id":4403,"country":"US","sunrise":1594892168,"sunset":1594945530},"timezone":-14400,"id":0,"name":"Mount Kisco","cod":200}
In the JSON response, the property weather is an array:
{..., weather: [{...}] }
but in your model it expects a MyResult type. Change it to expect an array of [MyResult]:
struct Response: Codable {
let weather: [MyResult]
}