#Edit - Thanks to the do-try-catch suggestion from Finn, it turned out Json was containing some null values, which I had missed during testing in postman. After refactoring fields in UserInfoStruct to Optionals, everything works.
I'm trying to unwrap json in my application, but despite getting code 200 from server and receiving bytes of data, printing returns nil. Putting field in struct that returns string with all the information resulted with nil as well, same with printing one of the fields. I've followed tutorials like the one on hackingwithswift.com, but to no avail. Previously checking on reqres.in endpoints, everything works just fine.
Code used to fetch data from api with the build-in json decoder:
func getUserInfo() {
var userInfo: UserInfoStruct?
let token = UserDefaults.standard.string(forKey: ApiUtils().userToken) ?? ""
guard let url = URL(string: EndpointUrls().machineUrl + EndpointUrls().profileUrl) else { return }
var request = URLRequest(url: url)
request.httpMethod = HttpMethodsStruct().get
request.setValue(ApiUtils().appjson, forHTTPHeaderField: ApiUtils().contType)
request.addValue(ApiUtils().bearer + token, forHTTPHeaderField: ApiUtils().auth)
URLSession.shared.dataTask(with: request) {data, response, error in
let httpStatusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
print(httpStatusCode)
if (httpStatusCode) == 200 {
if let data = data {
print(data)
userInfo = try? JSONDecoder().decode(UserInfoStruct.self, from: data)
print(userInfo)
}
}
}.resume()
}
I've replaced all the strings with variables declared in another struct, as some of them are used in the more places. With this, any changes would be easier to implement. To access the endpoints, You need to be logged in a specific vpn.
Here is the example json I'm receiving:
{
"authorities": [
{
"created_at": "2021-01-21T13:49:32.755Z",
"id": 0,
"name": "string",
"updated_at": "2021-01-21T13:49:32.755Z"
}
],
"business_name": "string",
"created_at": "2021-01-21T13:49:32.755Z",
"devices_limit": 0,
"email": "string",
"first_name": "string",
"has_active_call": true,
"id": 0,
"is_business_account": true,
"last_name": "string",
"marketing": true,
"min_to_call": 0,
"paid_to": "2021-01-21T13:49:32.755Z",
"password_reset_active_link": true,
"phone_number": "string",
"translation_types": [
{
"display_name": "string",
"id": 0,
"jabber_destination": "string",
"name": "string"
}
],
"updated_at": "2021-01-21T13:49:32.755Z"
}
Using app.quicktype.io, following structs has been generated:
import Foundation
struct UserInfoStruct: Codable {
let authorities: [Authority]
let business_name: String
let created_at: String
let devices_limit: Int
let email: String
let first_name: String
let has_active_call: Bool
let id: Int
let is_business_account: Bool
let last_name: String
let marketing: Bool
let min_to_call: Int
let paid_to: String
let password_reset_active_link: Bool
let phone_number: String
let translation_types: [TranslationType]
let updated_at: String
}
struct Authority: Codable {
let created_at: String
let id: Int
let name: String
let updated_at: String
}
struct TranslationType: Codable {
let display_name: String
let id: Int
let jabber_destination: String
let name: String
}
Still, in console all, that's printed, is this:
200 <- from print(httpStatusCode)
689 bytes <- from print(data)
nil <- from print(userInfo)
As in some tutorials, I've tried to declare userInfo field as
var userInfo = [UserInfoStruct]()
but it only resulted in
Cannot assign value of type 'UserInfoStruct?' to type '[UserInfoStruct]'
Declaring this field both in the func as well as #State in the main struct gave the same results.
About me: I'm self-taught with background in java. I've been developing in swift since October 2020, so I'm fully aware, that many problems might be trivial and my lack of experience causes me to miss them.
Thanks for all the help.
You can try catching your errors with a do-catch block instead of just using try?. This will enable you to log a error message instead if just getting a nil result.
a do-catch block is formed like this:
do {
userInfo = try JSONDecoder().decode(UserInfoStruct.self, from: data)
print("Success!")
} catch {
print("Unexpected error: \(error).")
}
Related
I am pretty new to Swift and as an exercise I try to create an App with Swift 5, which should show you the Weather at a Location you searched for.
Right now I am trying to implement a function that can turn a local JSON file in a struct I am using.
The JSON file is located at the same directory as all the other files.
The JSON for testing purposes looks like this:
[
{
"id": 833,
"name": "Ḩeşār-e Sefīd",
"state": "",
"country": "IR",
"coord": {
"lon": 47.159401,
"lat": 34.330502
}
},
{
"id": 2960,
"name": "‘Ayn Ḩalāqīm",
"state": "",
"country": "SY",
"coord": {
"lon": 36.321911,
"lat": 34.940079
}
}
]
The struct:
struct Root: Codable {
let jsonWeather: [JsonWeather]?
}
struct JsonWeather: Codable {
let id: Int
let name: String
let state: String
let country: String
let coord: Coord
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case state = "state"
case country = "country"
case coord = "coord"
}
}
The function that i am working on:
func loadJson(fileName: String) -> Void{
if let url = Bundle.main.url(forResource: fileName, withExtension: "json") {
do {
let data = try Data(contentsOf: url)
let jsonData = try JSONDecoder().decode(Array<Root>.self, from: data)
print(type(of: jsonData)) // Array<Root>
print(jsonData) // [WeatherApp.Root(jsonWeather: nil), WeatherApp.Root(jsonWeather: nil)]
} catch {
print("error:\(error)")
}
}
}
After all I wanted to see how the result looks like and I noticed that each jsonWeather in Root is nil
So in the end I understand that the values are nil because I allow them to be nil by setting an optional in the Root struct, but I dont quite understand why they are turning nil because there is data given. Also I don't know how I would implement it without being an optional.
This is my first time using JSONDecoder and Optionals in Swift.
Could anyone point out what I did wrong (or understood wrong) and how to fix this issue
Since your JSON is just of array of objects, you don't need a top-level struct like Root.
Get rid of your Root struct, and just decode an array of JsonWeathers:
let jsonData = try JSONDecoder().decode([JsonWeather].self, from: data)
(Note that there is no difference between [JsonWeather].self and Array<JsonWeather>.self - I just chose to do it this way since it's shorter.)
I have a very long Json array that is full of items that look like this:
[
{
"id": "sm10-1",
"name": "Pheromosa & Buzzwole-GX",
"imageUrl": "https://images.pokemontcg.io/sm10/1.png",
"subtype": "TAG TEAM",
"supertype": "Pokémon",
"hp": "260",
"retreatCost": [
"Colorless",
"Colorless"
],
"convertedRetreatCost": 2,
"number": "1",
"artist": "Mitsuhiro Arita",
"rarity": "Rare Holo GX",
"series": "Sun & Moon",
"set": "Unbroken Bonds",
"setCode": "sm10",
"text": [
"When your TAG TEAM is knocked out, your opponent takes 3 Prize Cards."
],
"types": [
"Grass"
],
"attacks": [
{
"name": "Jet Punch",
"cost": [
"Grass"
],
"convertedEnergyCost": 1,
"damage": "30",
"text": "This attack does 30 damage to 1 of your opponent's Benched Pokémon. (Don't apply Weakness and Resistance for Benched Pokémon.)"
},
{
"name": "Elegant Sole",
"cost": [
"Grass",
"Grass",
"Colorless"
],
"convertedEnergyCost": 3,
"damage": "190",
"text": "During your next turn, this Pokémon's Elegant Sole attack's base damage is 60."
},
{
"name": "Beast Game-GX",
"cost": [
"Grass"
],
"convertedEnergyCost": 1,
"damage": "50",
"text": "If your opponent's Pokémon is Knocked Out by damage from this attack, take 1 more Prize card. If this Pokémon has at least 7 extra Energy attached to it (in addition to this attack's cost), take 3 more Prize cards instead. (You can't use more than 1 GX attack in a game.)"
}
],
"weaknesses": [
{
"type": "Fire",
"value": "×2"
}
],
"imageUrlHiRes": "https://images.pokemontcg.io/sm10/1_hires.png",
"nationalPokedexNumber": 794
}
]
That is just one item of hundreds in the array. What I want to do is grab specific values from each item (i.e. name, imageUrl, supertype, hp, rarity, set) and send them to a struct which will then be added to an array of such structs.
What I currently have prints just prints out all of the json data and I can not figure out how to get individual data and create an array of structs for each individual card.
Here is the code I have currently:
//[TEST] READING JSON FILE LOCALLY
struct card: Decodable {
let name: String
let imageUrl: String
let supertype: String
let artist: String
let rarity: String
let set: String
let types: Array<String>
}
func loadJsonInfo() {
do{
let data = try Data.init(contentsOf: URL.init(fileURLWithPath: Bundle.main.path(forResource: "Unbroken Bonds", ofType: "json")!))
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(json)
} catch {
print(error)
}
}
Also, the json file is locally stored in my appData. Thanks in advance for your help!
Give a try to https://quicktype.io/
You put json there. And get all necessary data structures to decode json
To decode a JSON with Decodable type, you need to use JSONDecoder's decode(_:from:) method.
Update your loadJsonInfo() method to,
func loadJsonInfo() {
if let file = Bundle.main.url(forResource: "Unbroken Bonds", withExtension: "json") {
do {
let data = try Data(contentsOf: file)
let arr = try JSONDecoder().decode([Card].self, from: data)
print(arr)
} catch {
print(error)
}
}
}
Note: Use first letter capital while creating a type, i.e. use Card instead of card.
Parsing Code when you have DATA from server. I also removed force unwrapped ! so it won't crash in absence of file
func loadJsonInfo() {
if let path = Bundle.main.path(forResource: "Unbroken Bonds", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .alwaysMapped)
let result = try JSONDecoder().decode(ResultElement.self, from: data)
} catch let error {
print(error)
}
}
}
Your Model
import Foundation
// MARK: - ResultElement
struct ResultElement: Codable {
let id, name: String?
let imageURL: String?
let subtype, supertype, hp: String?
let retreatCost: [String]?
let convertedRetreatCost: Int?
let number, artist, rarity, series: String?
let resultSet, setCode: String?
let text, types: [String]?
let attacks: [Attack]?
let weaknesses: [Weakness]?
let imageURLHiRes: String?
let nationalPokedexNumber: Int?
enum CodingKeys: String, CodingKey {
case id, name
case imageURL = "imageUrl"
case subtype, supertype, hp, retreatCost, convertedRetreatCost, number, artist, rarity, series
case resultSet = "set"
case setCode, text, types, attacks, weaknesses
case imageURLHiRes = "imageUrlHiRes"
case nationalPokedexNumber
}
}
// MARK: - Attack
struct Attack: Codable {
let name: String?
let cost: [String]?
let convertedEnergyCost: Int?
let damage, text: String?
}
// MARK: - Weakness
struct Weakness: Codable {
let type, value: String?
}
typealias Result = [ResultElement]
I have to decode an array of the dictionary, where the key is an enum & value is a model object.
Here is my sample JSON,
[
{
"nanomp4": {
"url": "https://media.tenor.com/videos/a1da4dcf693c2187615721d866decf00/mp4",
"dims": [
150,
138
],
"duration": 2.0,
"preview": "https://media.tenor.com/images/17d523e6b7c3c9a4ca64566a1890d94d/tenor.png",
"size": 70381
},
"nanowebm": {
"url": "https://media.tenor.com/videos/aa983425114e32ab446f669d91611938/webm",
"dims": [
150,
138
],
"preview": "https://media.tenor.com/images/17d523e6b7c3c9a4ca64566a1890d94d/tenor.png",
"size": 53888
},
},
{
"nanomp4": {
"url": "https://media.tenor.com/videos/a1da4dcf693c2187615721d866decf00/mp4",
"dims": [
150,
138
],
"duration": 2.0,
"preview": "https://media.tenor.com/images/17d523e6b7c3c9a4ca64566a1890d94d/tenor.png",
"size": 70381
},
}
]
Here is my decoding code,
do {
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let collection = try decoder.decode([[GIFFormat:Media]].self, from: data)
print(collection)
} catch {
print("Error in parsing/decoding JSON: \(error)")
}
Here GIFFormat is Enum & Media is the model object, and they are decoding perfectly fine.
enum GIFFormat: String, Decodable {
case nanoMP4 = "nanomp4"
case nanoWebM = "nanowebm"
}
struct Media: Decodable {
let url: URL?
let dims: [Int]?
let duration: Double?
let preview: URL?
let size: Int64?
}
My console prints,
typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
Can anyone explain to me what is exactly wrong here?
Even though the rawValue for GIFFormat is String, GIFFormat itself is an enum. You should update
let collection = try decoder.decode([[GIFFormat:Media]].self, from: data)
to
let collection = try decoder.decode([[GIFFormat.RawValue:Media]].self, from: data)
UPDATE: In response to your comment
Now to access value, I need to use like this,
collection?.first?[GIFFormat.mp4.rawValue]?.url. Which is again ugly
!!
I would suggest a bit of refactoring. You can start by removing your enum altogether. Keep your Media struct. Create a new Collection struct
struct Collection: Decodable {
let nanomp4: Media!
let nanowebm: Media!
}
Then, you can update the above line to
let collection = try decoder.decode([Collection].self, from: data)
and your ugly line transforms into
collection.first?.nanomp4.url
NOTE: This solution assumes that you only have nanomp4 & nanowebm as your enum values. If this is not the case, then this might not be the best solution and you might have to go with the first solution.
i have 3 objects (nationalities, services and packages) and i got them from a JSON response as 3 arrays of these objects.. and i want to parse them from the JSON response ...
Classes are:
struct Root2 : Decodable {
let nationalities : [Nationalities]
let services : [Services]
let packages : [Packages]
}
struct Packages : Decodable {
let id: Int
let name: String
let idService: Int
let nationality: String
let totalPrice: Int
let visitsPerWeek: Int
let excludedDays: String
let excludedShifts: String
let excludedDates: String
let extraVisits: Int
let dateEnabled: String
let dateDisabled: String
let allowedFrom: String
let allowedTo: String
let visitType: String
let createdAt: String?
let updatedAt: String?
}
struct Nationalities : Decodable{
let id: Int
let name: String
let createdAt: String?
let updatedAt: String?
}
struct Services : Decodable{
let id: Int
let name, description: String
let createdAt: String?
let updatedAt: String?
}
and i have for each one another class like:
class Service : NSObject, NSCoding {
var id: Int
var name, desc: String
var createdAt: String?
var updatedAt: String?
init(id: Int, name: String, desc: String) {
self.id = id
self.name = name
self.desc = desc
}
required convenience init(coder aDecoder: NSCoder) {
let id = aDecoder.decodeInteger(forKey: "id")
let name = aDecoder.decodeObject(forKey: "name") as! String
let desc = aDecoder.decodeObject(forKey: "desc") as! String
self.init(id: id, name: name, desc: desc)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(id, forKey: "id")
aCoder.encode(name, forKey: "name")
aCoder.encode(desc, forKey: "desc")
}
}
and i'm using it like this:
func GetServicesPackagesNationalities(){
let link: String = "my link"
guard let url = URL(string: link) else {
print("Error: cannot create URL")
return
}
let urlRequest = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: urlRequest) {
(data, response, error) in
guard error == nil else {
print("error calling GET on /public/api/services")
print(error!)
return
}
guard let responseData = data else {
print("Error: did not receive data")
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder .decode(Root2.self, from: responseData)
var nationalities = [Nationality]()
for nationality in result.nationalities{
let newnationality = Nationality(id: nationality.id, name: nationality.name)
nationalities.append(newnationality)
print(newnationality.name)
}
var services = [Service]()
for service in result.services {
let newservice = Service(id: service.id, name: service.name, desc: service.description)
services.append(newservice)
print(newservice.name)
}
var packages = [Package]()
for package in result.packages{
let newpackage = Package(id: package.id, name: package.name, idService: package.idService, nationality: package.nationality, totalPrice: package.totalPrice, visitsPerWeek: package.visitsPerWeek, excludedDays: package.excludedDays, excludedShifts: package.excludedShifts, excludedDates: package.excludedDates, extraVisits: package.extraVisits ,dateEnabled: package.dateEnabled , dateDisabled: package.dateDisabled, allowedFrom: package.allowedFrom, allowedTo: package.allowedTo ,visitType: package.visitType)
packages.append(newpackage)
print(newpackage.name)
}
}catch {
print("error trying to convert data to JSON")
return
}
}
task.resume()
}
and i will always get this:
error trying to convert data to JSON
and my json is:
{
"nationalities": [
{
"id": 1,
"name": "Saudi Arabia",
"created_at": "2018-04-24 05:50:41",
"updated_at": "2018-04-24 06:35:29",
"deleted_at": null
},
{
"id": 2,
"name": "Bahrain",
"created_at": "2018-04-24 05:52:52",
"updated_at": "2018-04-24 05:52:52",
"deleted_at": null
},
],
"services": [
{
"id": 1,
"name": "Nad",
"description": "Nad ",
"created_at": null,
"updated_at": null,
"deleted_at": null
},
{
"id": 2,
"name": "Reay",
"description": "Re ",
"created_at": null,
"updated_at": null,
"deleted_at": null
},
],
"packages": [
{
"id": 1,
"name": "Gold Package",
"id_service": 1,
"nationality": "4",
"total_price": 1000,
"visits_per_week": 2,
"excluded_weekdays": "null",
"excluded_shifts": "null",
"excluded_dates": "1111-11-11",
"extra_visits": 1,
"date_enabled_from": "2018-04-01",
"date_enabled_to": "2018-04-30",
"date_allowed_from": "2018-04-05",
"date_allowed_to": "2018-04-30",
"visit_type": "Multi",
"created_at": "2018-04-26 11:18:45",
"updated_at": "2018-04-26 11:18:45",
"deleted_at": null
}
]
}
i don't know what i'm doing wrong .. since i tried the same with two objects and worked fine ..
can someone please tell me what im doing wrong?
using them in userdefaults:
storing:
let userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: services)
userDefaults.set(encodedData, forKey: "services")
userDefaults.synchronize()
retrieving:
let decoded = userDefaults.object(forKey: "services") as! Data
let decodedService = NSKeyedUnarchiver.unarchiveObject(with: decoded) as! [Service]
for service in decodedService {
print(service.name)
}
Please never ever print a meaningless literal string in a catch clause, print always the actual error, it tells you exactly what's wrong
} catch {
print(error)
}
In Package there are a lot of issues, here are the error messages (and the suggestions to fix them):
"No value associated with key CodingKeys(stringValue: \"excludedDays\", converted to excluded_days."
There is no key excluded_days in the JSON. Delete the property or declare it as optional (String?)
"No value associated with key CodingKeys(stringValue: \"dateEnabled\" converted to date_enabled."
The actual snake_case converted property is supposed to be dateEnabledFrom
"No value associated with key CodingKeys(stringValue: \"dateDisabled\" converted to date_disabled."
There is no key date_disabled in the JSON. Delete the property or declare it as optional (String?)
"No value associated with key CodingKeys(stringValue: \"allowedFrom\", intValue: nil) (\"allowedFrom\"), converted to allowed_from."
The actual snake_case converted property is supposed to be dateAllowedFrom
"No value associated with key CodingKeys(stringValue: \"allowedTo\", intValue: nil) (\"allowedTo\"), converted to allowed_to."
The actual snake_case converted property is supposed to be dateAllowedTo
Note:
Why do you use another extra classes for Nationality, Service, Package? They seem to be redundant. If you really need reference semantics decode the JSON into classes.
See the difference between Nationality and Nationalities
You should use Nationalities to parse your JSON response.
You should do like this, as you have declare Nationalities object in Root2
var nationalities = [Nationalities]()
for nationality in result.nationalities{
let newnationality = Nationalities(id: nationality.id, name: nationality.name)
nationalities.append(newnationality)
print(newnationality.name)
}
same for Services and Packages, Please find it.
I am trying to update myself and use modern and recent Swift 4 features.
That is why I am training with the Codable protocol in order to parse JSON and directly map my model object.
First of all, I did some research and self learning.
This article helped me a lot : Ultimate guide
I just need to focus on the "Com" array.
As you can notice, it contains some nested object. I named them Flash Info.
It is defined by :
endDate
text
image[]
title
productionDate
id
So here is my Codable Struct :
struct FlashInfo : Codable {
let productionDate: String
let endDate: String
let text: String
let title: String
let id: String
}
First of all, I was trying to parse it without the array of Images, I will handle it later.
So here is my method :
func getFlashInfo(success: #escaping (Array<FlashInfo>) -> Void) {
var arrayFlash = [FlashInfo]()
Alamofire.request(URL_TEST, method: .get).responseJSON { response in
if response.value != nil {
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
//let decoder = JSONDecoder()
// let flash = try! decoder.decode(FlashInfo.self, from: response.data!)
// arrayFlash.append(flash)
success(arrayFlash)
} else {
print("error getFlashInfo")
}
}
}
I don't know how to handle the fact that I only need the "Com" array and how to iterate through all nested objects in order to fill my array in the callback.
I mean, will the decode protocol iterate through each objects ?
Am I clear?
EDIT: JSON as text
{"Test": [], "Toto": [], "Com": [{"endDate": "2017-06-27T08:00:00Z", "text": "John Snow is getting married", "image": ["895745-test.png", "632568-test.png"], "titre": "We need you!", "productionDate": "2017-07-02T16:16:23Z", "id": "9686"}, {"endDate": "2017-07-27T08:00:00Z", "text": "LOL TEST", "image": ["895545-test.png", "632568-test.png"], "titre": "She needs you!", "productionDate": "2017-08-02T16:16:23Z", "id": "9687"},{"endDate": "2017-06-27T08:00:00Z", "text": "iOS swift", "image": ["895775-test.png", "638568-test.png"], "titre": "They need you!", "productionDate": "2017-07-02T16:16:23Z", "id": "9688"}], "Yt": []}
I believe the quickest way is just to define an incomplete Response type as well. For instance:
struct Response: Codable {
let Com: [FlashInfo]
}
struct FlashInfo: Codable {
let productionDate: String
let endDate: String
let text: String
let title: String
let id: String
let image: [String] = [] // Ignored for now.
enum CodingKeys: String, CodingKey {
case productionDate, endDate, text, id
case title = "titre" // Fix for JSON typo ;)
}
}
and decode it like this:
let decoder = JSONDecoder()
let response = try! decoder.decode(Response.self, from: data)
print(response.Com)
This worked great with the test data you provided (just watch out for the typo in the title field):
let json = """
{"Test": [], "Toto": [], "Com": [{"endDate": "2017-06-27T08:00:00Z", "text": "John Snow is getting married", "image": ["895745-test.png", "632568-test.png"], "titre": "We need you!", "productionDate": "2017-07-02T16:16:23Z", "id": "9686"}, {"endDate": "2017-07-27T08:00:00Z", "text": "LOL TEST", "image": ["895545-test.png", "632568-test.png"], "titre": "She needs you!", "productionDate": "2017-08-02T16:16:23Z", "id": "9687"},{"endDate": "2017-06-27T08:00:00Z", "text": "iOS swift", "image": ["895775-test.png", "638568-test.png"], "titre": "They need you!", "productionDate": "2017-07-02T16:16:23Z", "id": "9688"}], "Yt": []}
"""
let data = json.data(using: .utf8)!