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
}
Related
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.
How can I convert Codable to Json Dictionay its a nested struct
struct one
struct User: Codable {
var name = "siddhant"
var age = 12
var topInt = ["1","2","3"]
var moreDetails = MoreDetails()
}
struct Two
struct MoreDetails: Codable {
var image = "ImageUrl"
}
I need out put as a Json when I convert this to Json it just convert me the struct one not the 2nd struct
do {
let sid = try JSONEncoder().encode(users)
let dict = try JSONSerialization.jsonObject(with: sid, options: []) as? [String: Any]
print(dict)
}
catch {
print(error)
}
current output which is wrong:
(["name": siddhant, "topInt": <__NSArrayI 0x2831a1c20>(1,2,3),
"moreDetails": {
image = ImageUrl;
}, "age": 12])
I want output like
{
"name": "siddhant",
"topInt": ["1", "2", "3"],
"moreDetails": {
"image": "ImageUrl"
},
"age": 12
}
You can create an extension on Encodable so that you can use it for all elements that conform to Encodable protocol
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
In your case if the model is user of type User
guard let userDict = user.dictionary else { return }
what I did now is
let users = User()
let staticJson = ["name": users.name,
"age": users.age,
"topInt": users.topInt,
"moreDetails": ["image": users.moreDetails.image]] as? [String: Any]
You need to specify what is your desired result. Following your code:
struct User: Codable {
var name = "siddhant"
var age = 12
var topInt = ["1","2","3"]
var moreDetails = MoreDetails()
var dictionaryJson: [String : Any] {
let encoder = JSONEncoder()
return (try? JSONSerialization.jsonObject(with: encoder.encode(self), options: .allowFragments)) as? [String: Any] ?? [:]
}
}
struct MoreDetails: Codable {
var image = "ImageUrl"
}
do {
let users = User()
let jsonData = try JSONEncoder().encode(users)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
print("\n")
print(users.dictionaryJson)
print("\n")
print("Getting the var from dictionary")
print(users.dictionaryJson["moreDetails"])
} catch {
print(error)
}
The output:
{"age":12,"moreDetails":{"image":"ImageUrl"},"name":"siddhant","topInt":["1","2","3"]}
["topInt": <__NSArrayI 0x6000008e8150>(
1,
2,
3
)
, "name": siddhant, "age": 12, "moreDetails": {
image = ImageUrl;
}]
Getting the var from dictionary
Optional({
image = ImageUrl;
})
when i call API and get response from server with Alamofire, i want use "data" object from json
this data come from API
{
"code": 200,
"hasError": false,
"data": [
{
"userSession": "43a1bd70-26bf-11e9-9ccd-00163eaf6bb4"
}
],
"message": "ok"
}
and i want map data to my AuthModel
this is my AuthModel:
struct AuthModel: Codable {
let userSession: String
enum CodingKeys: String, CodingKey {
case userSession = "userSession"
}
}
i coded this lines but it isn't work:
if let responseObject = response.result.value as? Dictionary<String,Any> {
if let hasError = responseObject["hasError"] as? Bool {
guard !hasError else { return }
do {
let decoder = JSONDecoder()
let authModel = try decoder.decode(AuthModel.self, from: responseObject["data"])
} catch {
print("Parse Error: ",error)
}
}
}
this does not work because responseObject["data"] is not NSData Type
Cannot convert value of type '[String : Any]' to expected argument type 'Data'
I think your API response is a pattern that indicates:
Do we have any problem (error)?
Do we have our expected data?
Based on these, we can use Enum and Generics. For example:
class ResponseObject<T: Codable>: Codable {
private var code : Int
private var hasError : Bool
private var message : String
private var data : T?
var result: Result {
guard !hasError else { return .error(code, message) }
guard let data = data else { return .error(0, "Data is not ready.") }
return .value(data)
}
enum Result {
case error(Int, String)
case value(T)
}
}
and we can use ResponseObject with our expected data:
let responseString = """
{
"code": 200,
"hasError": false,
"data": [
{
"userSession": "43a1bd70-26bf-11e9-9ccd-00163eaf6bb4"
}
],
"message": "ok"
}
"""
class AuthObject: Codable {
var userSession : String
}
if let jsonData = responseString.data(using: .utf8) {
do {
//ResponseObject<[AuthObject]> means: if we don't have error, the `data` object in response, will represent `[AuthObject]`.
let responseObject = try JSONDecoder().decode(ResponseObject<[AuthObject]>.self, from: jsonData)
//Using ResponseObject.Result Enum: We have error with related code and message, OR, we have our expected data.
switch responseObject.result {
case .error(let code, let message):
print("Error: \(code) - \(message)")
case .value(let authObjects):
print(authObjects.first!.userSession)
}
} catch {
print(error.localizedDescription)
}
}
Get the Data response rather than the deserialized Dictionary for example
Alamofire.request(url).responseData { response in
and decode
let decoder = JSONDecoder()
let authModel = try decoder.decode(AuthModel.self, from: response.data!)
into these structs
struct AuthModel : Decodable {
let code : Int
let hasError : Bool
let message : String
let data : [Session]
}
struct Session : Decodable {
let userSession: String
}
All CodingKeys are synthesized.
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)
}
}
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)
}