How can I decode response data if data is nested in Swift - swift

I've got a problem with decoding response data. Here is my request function
#IBAction func onGetCities(_ sender: UIButton) {
guard let url = URL(string: "http://somelink.com/city-list") else { return }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let session = URLSession.shared
let task = session.dataTask(with: request) { (data, response, error) in
print(JSON(data))
guard let data = data else { return }
do{
let cities = try JSONDecoder().decode([City].self, from: data)
print(cities)
}catch{
}
}
task.resume()
}
And City struct
struct City: Decodable {
let id: Int
let city: String
}
Here is the response data, I want to decode "items"
{
"offset": 0,
"limit": 10,
"items": [
{id: 0, name: "City name"},
{id: 1, name: "City name1"},
.....
]
}

You need to have nested structures that mirror your nested JSON:
struct City: Decodable {
let id: Int
let city: String
}
struct ResponseObject: Decodable {
let items: [City]
let offset: Int
let limit: Int
}
And then:
do {
let result = try JSONDecoder().decode(ResponseObject.self, from: data)
print(result)
let cities = result.items
print(cities)
} catch {
print(error)
}
Note, in your original example, you included a JSON key, updated_at which was the date represented in number of seconds since 1970 (standard UNIX representation). So, to decode that:
struct City: Decodable {
let id: Int
let city: String
}
struct ResponseObject: Decodable {
let items: [City]
let offset: Int
let limit: Int
let code: Int
let updatedAt: Date
enum CodingKeys: String, CodingKey {
case items, offset, limit, code
case updatedAt = "updated_at"
}
}
And then:
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
do {
let result = try decoder.decode(ResponseObject.self, from: data)
print(result)
let cities = result.items
print(cities)
} catch {
print(error)
}

Related

Firebase - How do I read this map via embedded structs?

I am reading data from Firestore to be able to populate into expanding tableview cells. I have a really simple struct:
protocol PlanSerializable {
init?(dictionary:[String:Any])
}
struct Plan{
var menuItemName: String
var menuItemQuantity: Int
var menuItemPrice: Double
var dictionary: [String: Any] {
return [
"menuItemName": menuItemName,
"menuItemQuantity": menuItemQuantity,
"menuItemPrice": menuItemPrice
]
}
}
extension Plan : PlanSerializable {
init?(dictionary: [String : Any]) {
guard let menuItemName = dictionary["menuItemName"] as? String,
let menuItemQuantity = dictionary["menuItemQuantity"] as? Int,
let menuItemPrice = dictionary["menuItemPrice"] as? Double
else { return nil }
self.init(menuItemName: menuItemName, menuItemQuantity: menuItemQuantity, menuItemPrice: menuItemPrice)
}
}
And this is embedded in this struct:
protocol ComplainSerializable {
init?(dictionary:[String:Any])
}
struct Complain{
var status: Bool
var header: String
var message: String
var timeStamp: Timestamp
var email: String
var planDetails: Plan
var dictionary: [String: Any] {
return [
"status": status,
"E-mail": header,
"Message": message,
"Time_Stamp": timeStamp,
"User_Email": email,
"planDetails": planDetails
]
}
}
extension Complain : ComplainSerializable {
init?(dictionary: [String : Any]) {
guard let status = dictionary["status"] as? Bool,
let header = dictionary["E-mail"] as? String,
let message = dictionary["Message"] as? String,
let timeStamp = dictionary["Time_Stamp"] as? Timestamp,
let email = dictionary["User_Email"] as? String,
let planDetails = dictionary["planDetails"] as? Plan
else { return nil }
self.init(status: status, header: header, message: message, timeStamp: timeStamp, email: email, planDetails: planDetails)
}
}
However, I am not able to query any data from Firestore which looks like this:
Here is my query, although I am just reading all the files:
let db = Firestore.firestore()
var messageArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
self.messageArray = documentSnapshot!.documents.compactMap({Complain(dictionary: $0.data())})
for plan in self.messageArray {
print("\(plan.email)")
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
What am I doing wrong?
EDIT:
As suggested, here is the updated embedded struct:
import Foundation
// MARK: - Complain
struct Complain: Codable {
let eMail, message, timeStamp, userEmail: String
let status: Bool
let planDetails: PlanDetails
enum CodingKeys: String, CodingKey {
case eMail = "E-mail"
case message = "Message"
case timeStamp = "Time_Stamp"
case userEmail = "User_Email"
case status, planDetails
}
}
// MARK: - PlanDetails
struct PlanDetails: Codable {
let menuItemName: String
let menuItemQuantity: Int
let menuItemPrice: Double
}
Using quicktype.io, you can generate the struct. From there, all you need to do is run this tiny fragment of code within your response handler.
var compainArray = [Complain]()
func loadMenu() {
db.collection("Feedback_Message").getDocuments() { documentSnapshot, error in
if let error = error {
print("error:\(error.localizedDescription)")
} else {
guard let snapshot = documentSnapshot else {return}
for document in snapshot.documents {
if let jsonData = try? JSONSerialization.data(withJSONObject: document.data()){
if let converted = try? JSONDecoder().decode(Complain.self, from: jsonData){
self.compainArray.append(converted)
}
}
}
DispatchQueue.main.async {
self.testTable.reloadData()
}
}
}
}
Which will handle the looping, and mapping of certain variables. Let me know if you have any trouble with this.

Swift Codable: Use a parent's key as as value

I have a JSON that has ID's in the root level:
{
"12345": {
"name": "Pim"
},
"54321": {
"name": "Dorien"
}
}
My goal is to use Codable to create an array of User objects that have both name and ID properties.
struct User: Codable {
let id: String
let name: String
}
I know how to use Codable with a single root level key and I know how to work with unknown keys. But what I'm trying to do here is a combination of both and I have no idea what to do next.
Here's what I got so far: (You can paste this in a Playground)
import UIKit
var json = """
{
"12345": {
"name": "Pim"
},
"54321": {
"name": "Dorien"
}
}
"""
let data = Data(json.utf8)
struct User: Codable {
let name: String
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([String: User].self, from: data)
decoded.forEach { print($0.key, $0.value) }
// 54321 User(name: "Dorien")
// 12345 User(name: "Pim")
} catch {
print("Failed to decode JSON")
}
This is what I'd like to do:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([User].self, from: data)
decoded.forEach { print($0) }
// User(id: "54321", name: "Dorien")
// User(id: "12345", name: "Pim")
} catch {
print("Failed to decode JSON")
}
Any help is greatly appreciated.
You can use a custom coding key and setup User as below to parse unknown keys,
struct CustomCodingKey: CodingKey {
let intValue: Int?
let stringValue: String
init?(stringValue: String) {
self.intValue = Int(stringValue)
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
struct UserInfo: Codable {
let name: String
}
struct User: Codable {
var id: String = ""
var info: UserInfo?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CustomCodingKey.self)
if let key = container.allKeys.first {
self.id = key.stringValue
self.info = try container.decode(UserInfo.self, forKey: key)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CustomCodingKey.self)
if let key = CustomCodingKey(stringValue: self.id) {
try container.encode(self.info, forKey: key)
}
}
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(User.self, from: data)
print(decoded.id) // 12345
print(decoded.info!.name) // Pim
} catch {
print("Failed to decode JSON")
}

keyNotFound(CodingKeys(stringValue: "coord", intValue: nil)

I am building a small swift weather app using the openweatherAPI and I am running into some issues trying to parse the JSON. I have used the following function to parse the get and parse the json.
Below is my weather data struct:
struct WeatherData: Codable {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let id: Int
let name: String
let cod: Int
}
struct Clouds: Codable {
let all: Int
}
struct Coord: Codable {
let lon, lat: Double
}
struct Main: Codable {
let temp: Double
let pressure, humidity: Int
let tempMin, tempMax: Double
enum CodingKeys: String, CodingKey {
case temp, pressure, humidity
case tempMin = "temp_min"
case tempMax = "temp_max"
}
}
struct Sys: Codable {
let type, id: Int
let message: Double
let country: String
let sunrise, sunset: Int
}
struct Weather: Codable {
let id: Int
let main, description, icon: String
}
struct Wind: Codable {
let speed: Double
let deg: Int
}
private func getWeatherData(url: String, parameters: [String : String]) {
let JsonURLString:[String: Any] = ["url": WEATHER_URL, "parameters": parameters]
print(JsonURLString)
let urlString = JsonURLString["url"] as? String
guard let url = URL(string: urlString!) else { return }
URLSession.shared.dataTask(with: url) { ( data, response, err ) in
DispatchQueue.main.sync {
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
}
}.resume()
}
The exact error I am getting is the following:
longitude = -0.1337, latitude = 51.50998
["parameters": ["lat": "51.50998", "long": "-0.1337", "appid": "xxxxxxxxxxxxxxxxxx"], "url": "https://api.openweathermap.org/data/2.5/weather"]
keyNotFound(CodingKeys(stringValue: "coord", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"coord\", intValue: nil) (\"coord\").", underlyingError: nil))
I have looked at the following example and don't see how this would apply. Any help would be appreciated.
Icon is not appearing. Here is my model:
import UIKit
class WeatherDataModel {
//Declare your model variables here
var temperature: Int = 0
var condition: Int = 0
var city: String = ""
var weatherIconName = ""
var description: String = ""
//This method turns a condition code into the name of the weather condition image
func updateWeatherIcon(condition: Int) -> String {
switch (condition) {
case 0...300 :
return "tstorm1"
case 301...500 :
return "light_rain"
case 501...600 :
return "shower3"
case 601...700 :
return "snow4"
case 701...771 :
return "fog"
case 772...799 :
return "tstorm3"
case 800 :
return "sunny"
case 801...804 :
return "cloudy2"
case 900...903, 905...1000 :
return "tstorm3"
case 903 :
return "snow5"
case 904 :
return "sunny"
default :
return "dunno"
}
}
}
I have added my own icons. I have added this in the do catch block.
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
print(city)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.weatherData.weatherIconName = WeatherDataModel.updateWeatherIcon(self.weatherData.condition)
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
The error I am getting this error now:
Instance member 'updateWeatherIcon' cannot be used on type 'WeatherDataModel'; did you mean to use a value of this type instead?
You are creating only the openweathermap URL but you ignore the parameters.
Use something like this for example URLComponents and URLQueryItem to build the URL query properly
private func getWeatherData(parameters: [String : String]) {
guard let lat = parameters["lat"],
let long = parameters["long"],
let appID = parameters["appid"] else { print("Invalid parameters"); return }
var urlComponents = URLComponents(string: "https://api.openweathermap.org/data/2.5/weather")!
let queryItems = [URLQueryItem(name: "lat", value: lat),
URLQueryItem(name: "lon", value: long),
URLQueryItem(name: "appid", value: appID)]
urlComponents.queryItems = queryItems
guard let url = urlComponents.url else { return }
URLSession.shared.dataTask(with: url) { ( data, response, err ) in
DispatchQueue.main.async { // never, never, never sync !!
if let err = err {
print("Failed to get data from url:", err)
return
}
guard let data = data else { return }
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let city = try decoder.decode(WeatherData.self, from: data)
print(city)
self.weatherData.description = city.weather[0].description
self.weatherData.temperature = Int(city.main.temp - 273)
self.weatherData.city = city.name
self.weatherData.condition = city.weather[0].id
self.updateUIWeatherData()
} catch {
print(error)
self.cityLabel.text = "Connection issues"
}
}
}.resume()
}
and pass only
["lat": "51.50998", "long": "-0.1337", "appid": "xxxxxxxxxxxxxxxxxx"]
as parameters.

Swift - How to init an array in struct?

I'm not sure how to init the array in Struct. I'm not able to fetch data from array, meanwhile I was manage to get the result from object (platform.status).
Am I init it wrongly ?
Any ideas ?
Here is Network Request :
func fetchServicePlatform(token: String, _key: String) {
let selectedUrl = URL(string: "\(mainUrl)/get_service")
let parameters: [String: String] = ["_key": _key]
var serviceList = [ServiceList]()
URLSession.shared.dataTask(with: setupURLRequest(selectedURL: selectedUrl!, parameters: parameters, token: token, email: "test#gmail.com")) { (data, response, error) in
if let err = error {
print("Failed to fetch API: \(err.localizedDescription)")
}
guard let data = data else { return }
do {
guard let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] else { return }
let platform = Platform(json: json)
if platform.status == "success" {
self.serviceList = platform.service_list
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
} catch let jsonErr {
print("Failed to fetch service platform: ", jsonErr.localizedDescription)
}
}.resume()
}
Here is JSON :
{
"status": "pass",
"service_list": [
{
"title": "Plumber",
"description": "Plumber",
"image": "https://s3-ap-southeast-1.heroku.com.png"
},
{
"title": "Cleaner",
"description": "Cleaner",
"image": "https://s3-ap-southeast-1.heroku.com.png"
}
]
}
Here is Struct :
struct Platform {
let service_list: [ServiceList]
let status: String
init(json: [String: Any]) {
service_list = [ServiceList(json: json["service_list"] as? [String: Any] ?? [:])]
status = json["status"] as? String ?? ""
}
}
struct ServiceList {
let title: String
let description: String
let image: String
init(json: [String: Any]) {
title = json["title"] as? String ?? ""
description = json["description"] as? String ?? ""
image = json["image"] as? String ?? ""
}
}
In your data json["service_list"] is an array of dictionaries,
You Can try out.
struct Platform {
var service_list: [ServiceList] = []
var status: String
init(json: [String: Any]) {
if let jsonArray = json["service_list"] as? [[String: Any]] {
for service in jsonArray {
service_list.append(ServiceList(json: service))
}
}
else{
service_list = []
}
status = json["status"] as? String ?? ""
}
}
init an array in the struct
struct MyData {
var dataArray:[Any] = []
var latitude: Float
var longitude: Float
}
You have to unwrap the dictionary as an array of dictionaries and then loop through it with a map or flatmap, where you use the value $0 as the value
guard let serviceList = (json["service_list"] as? [[String: Any]])?.flatmap({ServiceList(withDictionary: $0)})
Go with the below approach, this way it is simpler
Create your structure with Codable
struct Platform: Codable {
let service_list: [ServiceList]
let status: String
enum CodingKeys: String, CodingKey {
case service_list, status
}
}
struct ServiceList: Codable {
let title: String
let description: String
let image: String
enum CodingKeys: String, CodingKey {
case title, description, image
}
}
Your json data object
let json = """
{
"status": "pass",
"service_list": [
{
"title": "Plumber",
"description": "Plumber",
"image": "https://s3-ap-southeast-1.heroku.com.png"
},
{
"title": "Cleaner",
"description": "Cleaner",
"image": "https://s3-ap-southeast-1.heroku.com.png"
}
]
}
""".data(using: .utf8)!
Use the JSONDecoder to map the json object to structure
let decoder = JSONDecoder()
let platform = try! decoder.decode(Platform.self, from: json)
if platform.status == "pass"{
for service in platform.service_list{
print(service.title)
}
}

Decode an array inside JSON data only using Swift 4 Codable

I'm using Swift 4 Codable and I'm receiving this JSON from my web service:
{
"status": "success",
"data": {
"time": "00:02:00",
"employees": [
{
"id": 001,
"name": "foo"
},
{
"id": 002,
"name": "bar"
}
]
}
}
I want to decode only employees array into employee objects (the time property will only be saved once), but nothing works.
I read a lot of materials about Swift 4 Codable but don't get how can I decode this array.
EDIT: My employee class:
import Foundation
struct Employee: Codable {
var id: Int
var time: Date
enum CodingKeys: String, CodingKey {
case id = "id"
case time = "time"
}
}
The request:
Alamofire.SessionManager.default.request(Router.syncUsers)
.validate(contentType: ["application/json"])
.responseJSON { response in
if response.response?.statusCode == 200 {
guard let jsonDict = response as? Dictionary<String, Any>,
let feedPage = Employee(from: jsonDict as! Decoder) else {
return
}
guard let response = response.result.value as? [String: Any] else {
return
}
guard let data = response["data"] as? [String: Any] else {
return
}
guard let users = data["employees"] else {
return
}
guard let usersData = try? JSONSerialization.data(withJSONObject: users, options: .prettyPrinted) else {
return
}
guard let decoded = try? JSONSerialization.jsonObject(with: usersData, options: []) else {
return
}
let decoder = JSONDecoder()
guard let employee = try? decoder.decode(Employee.self, from: usersData) else {
print("errorMessage")
return
}
} else {
print("errorMessage")
}
}
When using Codable you cannot decode inner data without decoding the outer.
But it's pretty simple, for example you can omit all CodingKeys.
struct Root : Decodable {
let status : String
let data : EmployeeData
}
struct EmployeeData : Decodable {
let time : String
let employees : [Employee]
}
struct Employee: Decodable {
let id: Int
let name: String
}
let jsonString = """
{
"status": "success",
"data": {
"time": "00:02:00",
"employees": [
{"id": 1, "name": "foo"},
{"id": 2, "name": "bar"}
]
}
}
"""
do {
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode(Root.self, from: data)
for employee in result.data.employees {
print(employee.name, employee.id)
}
} catch { print(error) }
You can solve this kind of JSON parsing problem very easily with quicktype. Just paste in your JSON on the left and you'll get types and serialization/deserialization code on the right, for a variety of languages. Here are the types it produces for your JSON:
struct Employees: Codable {
let status: String
let data: EmployeesData
}
struct EmployeesData: Codable {
let time: String
let employees: [Employee]
}
struct Employee: Codable {
let id: Int
let name: String
}