Swift 4 - JSON parsing with Codable protocol (nested data) - ios

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)!

Related

Error with creating an Array of JSON objects in swift

I am trying to parse a JSON string which is a collection of multiple 'Countries'. I have created a structure representing the fields in each JSON object and then one for the array. However I keep getting the following error
Error: cannot convert value of type 'CountryList' to specified type 'Country'
Here is my Code (written in Swift Playgrounds)
import Cocoa
let body = """
{"Countries":
[
{
"id": 1,
"name": "USA",
"capitalCity": 5,
},
{
"id": 2,
"name": "France",
"capitalCity": 5,
}]}
""".data(using: .utf8)
struct Country: Codable {
let id: Int
let name: String
let capitalCity: Int
}
struct CountryList: Codable {
var Countries: [Country]
}
let countryJson: Country = try! JSONDecoder().decode(CountryList.self, from: body!)
print(countryJson)
Thank you for your help.
Replace this line
let countryJson: Country = try! JSONDecoder().decode(CountryList.self, from: body!)
with this:
let countryJson: CountryList = try! JSONDecoder().decode(CountryList.self, from: body!)

Getting nil after decoding JSON in Swift 5

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

Values of Array of struct are nil

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.)

Create array of Json Data Items

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]

Swift 4 Decodable: Decoding complex JSON

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.

Resources