How to use nested json with structs in swift - ios

I have an API and I need to call, to get a list of holidays with some additional info along with it. The link of my API - http://mahindralylf.com/apiv1/getholidays
The structure I created using the website app.quicktype.io
struct Holiday: Codable {
let responseCode, responseMsg: String
let holidayCount: Int
let holidays: [HolidayElement]
enum CodingKeys: String, CodingKey {
case responseCode = "response_code"
case responseMsg = "response_msg"
case holidayCount = "holiday_count"
case holidays
}
}
struct HolidayElement: Codable {
let month: String
let image: String
let details: [Detail]
}
struct Detail: Codable {
let title, date, day: String
let color: Color
}
enum Color: String, Codable {
case b297Fe = "#B297FE"
case e73838 = "#E73838"
case the0D8464 = "#0D8464"
}
I can get to the "Holiday" object, print it, display my tableViewCells with a colour for the "holidayCount". What I want to do is, without using the normal json parsing and making my own arrays and dicts, to access the "Detail" for each "holidays".
tl;dr - I need to know how to access Detail for the holidays element
Thanks!!

Your data's coming back with an array of HolidayElements and each HolidayElement has an array of Details.
So for each HolidayElement, you'd want to get access to the details array. You'd do that like so:
let jsonResponse = try JSONDecoder().decode(Holiday.self, from: responseData)
print(jsonResponse.holidays[0].details)
Here's a repo to play around with.
Additionally, your coding keys are just converting from snake_case, so you don't really need them for that endpoint. Instead, you can just tell the decoder to convertFromSnakeCase
You can ditch the coding keys in this case and just decode as follows:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let jsonResponse = try decoder.decode(Holiday.self, from: responseData)
print(jsonResponse.holidays[0].details)

Related

what is the best way to save json data in swift model

I previously made a post about how to save json data in a model here .
Good now I am developing a project on iOS with swift, and I face the following problem, it happens that sometimes the database administrators change the names of the columns constantly, I only consume services with the help of Alamofire, to save data in models, use camal case and snake case, At the moment everything is fine but I would like to know what is the best way to save json data in a swift model, in my experience with Android I used retrofit with #Serializename and it worked great because if the json attribute of the service it was modified I only had to update a line of code and my variable could be kept the same, this helped me maintain a better order and it made it scalable.
In some cases the json comes to me.
{
"price": "385.000000",
"nameusser": null,
"favorite": 43,
"short_nameProduct": "Génifique Repair Sc",
"description_product": "Génifique repair sc es la crema de noche antiedad de lancôme. Despiértese con una piel fresca y rejuvenecida con nuestra crema de noche.",
"alt": null,
"photo": "https://url/020021000112-1.png"
}
in swift it would generate my model in the following way.
struct Product : Codable {
let price : String?
let nameusser : String?
let favorite : Int
let shortNameProduct : [Product]
let description : [Galery]
let alt : Product
let success : Bool
}
The problem here is that my variables must fit the json I get to use the JSONDecoder() and the convertFromSnakeCase, I can not define them myself.
while in java android I just have to do it like that.
#SerializedName("price")
private String price;
#SerializedName("nameusser")
private String name;
#SerializedName("favorite")
private Int favorite;
#SerializedName("short_nameProduct")
private String shortName;
#SerializedName("description_product")
private String descriptionProduct;
#SerializedName("altitude")
private String altitude;
#SerializedName("photo")
private String photo;
I just have to create the get and set and I would be ready to use the model.
I need to know how to do in swift the same, maybe a library that helps me store data json in the same way that I do in android.
Any comment would be very appreciated.
The best way is to use the Coding Keys:
struct Product : Codable {
let price : String?
let nameusser : String?
let favorite : Int
let shortNameProduct : [Product]
let description : [Galery]
let alt : Product
let success : Bool
enum CodingKeys: String, CodingKey {
case price = "price"
case nameusser = "nameusser"
case favorite = "favorite"
case shortNameProduct = "short_nameProduct"
case description = "description_product"
case alt = "alt"
case success = "success"
}
}
The name of the enum case has to match the property name on the struct. This way you can define whatever keys you want without having to write any custom encoding or decoding code.
Feel free to use my gist here:
Storage
You can use it like this:
let fileName = "Product.json"
extension Product {
func store() {
Storage.store(self, to: .documents, as: fileName)
}
static func retrieve() -> Product? {
guard let product = Storage.retrieve(fileName, from: .documents, as: Product.self) else {
return nil
}
return product
}
}
With Alamofire you will get a key-value, an Dictionary<String,Any> or [String:Any]
So you can do the following with your dictionary:
var myProduct : Product?
if let price = myDictionary["price"] as? String{
myProduct.price = price
}
With that example you can create a method to mapping the entire JSON into your struct. Maybe if you want to make it more scalable, you can create an enum with String raw values and use it as the key for the dictionary, some like:
enum productProperty : String{
case Price = "price"
}
var myProduct : Product?
if let price = myDictionary[productProperty.Price] as? String{
myProduct.price = price
}
And maybe create a more complex class to iterate trough the dictionary and check the key using the enum, but that implementation depends of your own skills.
Edit1:
To use't with alamofire, you need to get the jsonResponse of the request, some like that:
.request(yourURL, method: .get, parameters: yourParameter).responseJSON(options: .allowFragments , completionHandler: { response in
if let object = response.result.value as? Dictionary<String,Any> {
yourMethodToSave(object)
}
})
and inside yourMethodToSave(_ object: Dictionary<String,Any>) you need to put the logic above.
Ps: The #sendtobo answer have the enum example that i tell that you can use to a more scalable mapping for your object

trying to decode value in swift 4.2 using its position in the json structure

How can I decode the following using swift Decodable? I'm only interested in the extract value
{
"batchcomplete":"",
"query":{
"normalized":[ ],
"pages":{
"434325":{ //can be any number!
"pageid":434325,
"ns":0,
"title":"asdfasdfsa",
"extract":""
I'm attempting the following:
struct Entry: Decodable {
let batchcomplete: String
let query: Query
struct Query: Decodable {
let normalized: [String]
let pages: Page
struct Page: Decodable {
let pageid: Entry // I think this is going to be an issue
struct Entry: Decodable {
let title: String
let extract: String
}
}
}
}
I'm trying to retrieve the extract like this:
print(entry.query.pages.first.extract)
Is there a way to use .first for this ?
I'm only every going to get maximum one page so ideally I would just grab the first element.
Your decoding code is ok up until `Query type.
So what should be used instead:
struct Query: Decodable {
let normalized: [String]
let pages: [String: Page]
struct Page: Decodable {
let pageid: Int
let title: String
let extract: String
}
}
So, key points:
If you don't know what keys would be there, use [String: <SomeDecodableType>] because any JSON Object can be mapped to the [String: <Decodable>].
You don't need to declare Page.Entry, just put all the fields to the Page
To get extract instead of entry.query.pages.first.extract you will have to use entry.query.pages.first?.value.extract (extract of first random page, because [String: Page] is unsorted) or entry.query.pages.sorted(by: sorter).first?.value.extract (where sorter is some function, that takes two key-value pairs and returns true if first one should go before second one in order) to get first using some order.

Ios how to post multiple Custom objects using Alamofire

Hi i am new to iOS and I am using Alamofire for network calls. The things were going good and I am facing no trouble in making network calls. But since I have to post my custom object I am having no luck. so here are the things I was doing before
let parameters: Parameters = [
"Phone": phone,
"ApiKey":"x-y-z"
]
this was working fine.
but now I have to post my objects like
let parameters: Parameters = [
"ApiKey": Common.API_KEY,
"cardModel": cardModel,
"clientModel" : clientModel
]
My cardModel and client model are already converted in Json string i am just putting them into dictionary. the converted model looks like these
"cardModel": {
"ExpiryYear": 2018,
"ExpiryMonth": 1,
"CardNumber": "55555",
"CardHolderName": "xyz"
}
so I am putting these serialized models in the dictionary and post this data into request body using Alamofire.
But on server side these Models are null. Any idea how to put custom model in the way I want ? please help
PS I just print out my parameters dictionary and I have examined this output
["ApiKey": "x-y-z",
"\cardModel\": "{
"\ExpiryYear\": 2018,
"\ExpiryMonth\": 1,
"\CardNumber\": "\55555\",
"\CardHolderName\": "\xyz\"
}
]
I put that parameters json printed output in jsonLint and it was wrong format. I just removed the "\" and replaced [] with {} and then it appears to be valid Json
So what I should do now???? please help
Update1:
this is valid json for my endpoint (sending from android)
{
"ApiKey": "XXX-XXX-XXX",
"cardModel": {
"CardHolderName": "Fjj",
"CardNumber": "555",
"CardExpiryMonth": 1,
"CardExpiryYear": 2018
......
}
}
First you need to have a method/computed property that converts your model into a dictionary of type [String: AnyObject] or [String: Any].
Then instead of adding your model into your parameter dictionary add the model dictionary like in below example.
class CardModel: NSObject {
var expiryYear: Int?
var expiryMonth: Int?
var cardNumber: String?
var cardHolderName: String?
var jsonDict: [String: AnyObject] {
var json: [String: AnyObject] = [:]//Initializing an Empty Dictionary
json["expiryYear"] = expiryYear as AnyObject // Inserting expiryYear into Dictionary
json["expiryMonth"] = expiryMonth as AnyObject // Inserting expiryMonth into Dictionary
json["cardNumber"] = cardNumber as AnyObject // Inserting cardNumber into Dictionary
json["cardHolderName"] = cardHolderName as AnyObject // Inserting cardHolderName into Dictionary
return json // returning newly populated dictionary
}
}
func requestToServer(cardModel: CardModel) {
var parameters = [String: AnyObject]()
parameters["ApiKey"] = "dfv12345df234t" as AnyObject
parameters["cardModel"] = cardModel.jsonDict as AnyObject// adding card model dictionary into you paramters dictionary.
//Same logic will be use for your next clientModel
}
I suggest you read up on the Codable protocol, it is a very elegant way to map basic Swift-types to JSON-data. A Playground will help you with the following
import Cocoa
let jsonData = """
{
"ApiKey": "XXX-XXX-XXX",
"cardModel": {
"CardHolderName": "Fjj",
"CardNumber": "555",
"CardExpiryMonth": 1,
"CardExpiryYear": 2018
}
}
""".data(using: .utf8)!
// ... missing here (but added a closing brace for cardModel
struct CardModel: Codable {
let holderName: String
let number: String
let expiryMonth: Int
let expiryYear: Int
enum CodingKeys: String, CodingKey {
case holderName = "CardHolderName"
case number = "CardNumber"
case expiryMonth = "CardExpiryMonth"
case expiryYear = "CardExpiryYear"
}
}
struct ApiCardPayment: Codable {
let apiKey: String
let cardModel: CardModel
enum CodingKeys: String, CodingKey {
case apiKey = "ApiKey"
case cardModel
}
}
do {
let apiPayment = try JSONDecoder().decode(ApiCardPayment.self, from:jsonData)
print(apiPayment)
} catch {
print(error)
}
This is much easier to handle than the [String:AnyObject] casting nightmare you will probably have to interpret otherwise. Besides the error messages of the JSONDecoder have been improving rapidly and it is now rather good at pointing out what is going wrong.
P.S.: Of course there is also JSONEncoder().encode(), but that is just the easy part anyways.

How to pass model class data from one page another in iOS?

Here I won't have any connection for both the pages and here I need to save the model class globally and to use anywhere in all pages till app was in use and after it may can clear the data having in array but I can able to access anywhere in all pages in app and I tried using to save in UserDefaults it crashed. Can anyone help me how to implement this?
var sortModel = [Sort]()
for (_, value) in sortJson as! [String: Any] {
self.sortModel.append(Sort.init(dict: value as! [String : Any]))
}
UserDefaults.standard.set(self.sortModel, forKey: "sorts")
Get your Sort struct to conform to Codable like:
struct Sort: Codable {
//...
}
then you can quickly get away with:
//Encode Sort to json data
if let jsonData = try? JSONEncoder().encode(sortModel) {
print("To Save:", jsonData)
//Save as data
UserDefaults.standard.set(jsonData,
forKey: "sorts")
}
//Read data for "sorts" key and decode to array of "Sort" structs
if let data = UserDefaults.standard.data(forKey: "sorts"),
let sorts = try? JSONDecoder().decode([Sort].self,
from: data) {
print("Retrieved:", sorts)
}
Basically, we make a json out of the array and save it as a data object.
We then can get it back as data and recreate the sort struct array.
NOTE: This may not work if the struct has nested within itself some types that prevent it from getting encoded as a json.
In this case, read:
http://swiftandpainless.com/nscoding-and-swift-structs

JSONSerialization of Custom Object Array

I have a custom class
public class Balance: NSObject {
var details: String
var date: Date
var amount: Double
}
I have tried as a struct and as a class both fail
I have an array of balances list: [Balance]
Now I'm need to convert this array into a JSON String
something like
[ {details = "text"; date = "2016-11-20"; amount = 0;} ,
{details = "text2"; date = "2016-11-25"; amount= 10;} ]
I also need to be able to convert the String back into the array.
But I can't even get the array to JSON string to work
var resStr = ""
var list: [Balance]
do {
let data = try JSONSerialization.data(withJSONObject: list, options: .prettyPrinted)
resStr = String.init(data: data, encoding: .utf8) ?? ""
} catch { fatalError("Error BALANCE HISTORY ") }
Fails with
'Invalid type in JSON write (Balance)'
Please advise
Thanks in advance
What about defining your class like this:
public class Balance: NSObject {
var details: String
var date: Date
var amount: Double
func toJSONString() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
return "{details = \(self.details); date = \(dateFormatter.string(from: self.date)); amount = \(self.amount);}"
}
}
Then you can create the full JSON string like this:
var fullJSONArray = [String]()
for balance in list {
fullJSONArray.append(balance.toJSONString)
}
let fullJSONString = fullJSONArray.description
I hope this helps you out! Good luck and let me know if you have any further questions or issues!
You can only store a quite small list of data types to JSON.
#CoolPenguin offers one solution - a custom method that will convert your object to a JSON string.
I would advise against building JSON strings yourself.
Instead, I would suggest creating a toDictionary() method for your class, and an init(dictionary:) method to create a Balance object from a dictionary.
You can convert the dates to TimeIntervals since 1970 (the UNIX "epoch date".) That seems easier than converting them to date strings.
You can then map your array of Balance objects to an array of dictionaries, and convert that to JSON using normal JSONSerialization.
let mappedArray = balanceArray.map{$0.toDictionary()}
And then easily convert your array of dictionaries to JSON.
Since others have pointed out that you can achieve the desired effect by first converting your object to a Dictionary, I will provide a method to achieve what is required by using JSONEncoder instead.
If you maintain the values you need to represent in the JSON encoded string (for example the date as a formatted String), the only required bits are,
Make your type conform to Codable
Create a CodingKeys enum that represent the JSON keys for your type's properties.
Please see below for an example that applies to your object.
public class Balance: NSObject, Codable {
var details: String
// Used to represent formatted date.
var dateString: String
var date: Date = Date(){
didSet{
updateDateString()
}
}
var amount: Double
enum CodingKeys: String, CodingKey{
case details, dateString, amount
}
init(_ d: String, amt: Double){
details = d
dateString = ""
date = Date()
amount = amt
super.init()
updateDateString()
}
private func updateDateString(){
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = "yyyy-MM-dd"
dateString = df.string(from: date)
}
}
var arr: [Balance] = [
Balance("Shared Balance", amt: 100),
Balance("Coupon Balance", amt: 120),
Balance("Account Balance", amt: 150)
]
do{
let data = try JSONEncoder().encode(arr)
// Convert to JSON Encoded String
let str = String(data: data, encoding: .utf8)!
print(str)
// Convert back from JSON Encoded String
let raw = try JSONDecoder().decode([Balance].self, from: str.data(using: .utf8)!)
print(raw)
}
catch(let err){
print(err)
}
The additional updateDateString() boiler plate code is to produce the String with the date format, "yyyy-MM-dd".
The above code can product a JSON encoded version of your Balance array as follows.
[{"details":"Shared Balance","amount":100,"dateString":"2021-01-14"},
{"details":"Coupon Balance","amount":120,"dateString":"2021-01-14"},
{"details":"Account Balance","amount":150,"dateString":"2021-01-14"}]
The key takeaway is to use JSONEncoder instead of JSONSerialization.
Hope I helped!
Well, I believe first you need to convert your object to some form of dictionary.
Let me show you an example:
class Balance: NSObject {
var details: String
var date: Date
var amount: Double
func asDictionary() -> [String: AnyObject] {
return ["details": details as AnyObject, "date": "\(date)" as AnyObject, "amount": amount as AnyObject]
}
}
You use the method asDictionary to convert your objects to a dictionary so that you can serialize it into JSON.
Suppose you have a list of Balance objects.
You need to first convert each of those objects to dictionary using the method above, and then try to serialize the objects to JSON. Note that the list is now a list of [String: AnyObject] dictionaries, and not a list of Balance objects.
var resStr = ""
var list: [[String: AnyObject]] = [balance1.asDictionary(), balance2.asDictionary()]
do {
let data = try JSONSerialization.data(withJSONObject: list, options: .prettyPrinted)
resStr = String.init(data: data, encoding: .utf8) ?? ""
} catch { fatalError("Error BALANCE HISTORY ") }
For certain types, like the date field, you need to find some way to convert it to String, as the JSONSerializer is very picky. In this case I just used String interpolation, but the format may not be what you want it to be like.
If you want to convert back to a Balance object from JSON, first you need to get JSON into a dictionary, and then check for each field if it exists, and if so, construct your Balance object.
Supposing you have converted your JSON data into a dictionary in the variable named dict, you could do something like the following:
// supposing you have a single object in dict
var balance: Balance
balance.details = dict["details"]
balance.amount = dict["amount"]
balance.date = parseDate(dict["date"])
Supposing you have a function parseDate to parse the date from String into a Date object.
You can take a look here for converting String date into an object: Use SwiftyJSON to deserialize NSDate

Resources