Swift custom decoding including another custom decoded model - ios

I am currently building a food app using Yelp Fusion API. There are two models called Business and BusinessDatail in my project. To list up restaurants in a tableView, I fetch a restaurant list from backend and custom decode it as an array of Business, [Business]. And then, when a tableViewCell of Business is clicked, I fetch detailed business info and custom decode it as a BusinessDetail.
struct Business {
let id: String
let name: String
let rating: Float?
let price: String?
let displayPhone: String?
let imageUrl: URL
let category: String
let reviewCount: Int
let coordinates: Coordinates
let address: Address
}
struct BusinessDetail {
let id: String
let name: String
let rating: Float?
let price: String?
let displayPhone: String?
let imageUrl: URL
let category: String
let reviewCount: Int
let coordinates: Coordinates
let address: Address
let isClaimed: Bool
let isClosed: Bool
let url: URL
let phone: String?
let photoURLs: [URL]
var hours: [Hours]?
}
Both Business and BusinessDetail are custom decoded models and they are currently initialised this way.
extension Business: Decodable {
enum CodingKeys: String, CodingKey {
case id
case name
case rating
case price
case displayPhone = "display_phone"
case imageUrl = "image_url"
case category = "categories"
case reviewCount = "review_count"
case coordinates
case address = "location"
enum CategoryCodingKeys: String, CodingKey {
case title
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
rating = try container.decode(Float.self, forKey: .rating)
price = try container.decodeIfPresent(String.self, forKey: .price)
displayPhone = try container.decodeIfPresent(String.self, forKey: .displayPhone)
imageUrl = try container.decode(URL.self, forKey: .imageUrl)
var categoriesContainer = try container.nestedUnkeyedContainer(forKey: .category)
var tempCategory = ""
while !categoriesContainer.isAtEnd {
let categoryContainer = try categoriesContainer.nestedContainer(keyedBy: CodingKeys.CategoryCodingKeys.self)
let title = try categoryContainer.decode(String.self, forKey: .title)
tempCategory += tempCategory == "" ? title: ", \(title)"
}
category = tempCategory
reviewCount = try container.decode(Int.self, forKey: .reviewCount)
coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
address = try container.decode(Address.self, forKey: .address)
}
}
extension PlaceDetail: Decodable {
enum CodingKeys: String, CodingKey {
case id
case name
case rating
case price
case displayPhone = "display_phone"
case imageUrl = "image_url"
case category = "categories"
case reviewCount = "review_count"
case coordinates
case address = "location"
enum CategoryCodingKeys: String, CodingKey {
case title
}
case isClaimed = "is_claimed"
case isClosed = "is_closed"
case url
case phone
case photoURLs = "photos"
case hours
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
rating = try container.decode(Float.self, forKey: .rating)
price = try container.decodeIfPresent(String.self, forKey: .price)
displayPhone = try container.decode(String.self, forKey: .displayPhone)
imageUrl = try container.decode(URL.self, forKey: .imageUrl)
var categoriesContainer = try container.nestedUnkeyedContainer(forKey: .category)
var tempCategory = ""
while !categoriesContainer.isAtEnd {
let categoryContainer = try categoriesContainer.nestedContainer(keyedBy: CodingKeys.CategoryCodingKeys.self)
let title = try categoryContainer.decode(String.self, forKey: .title)
tempCategory += tempCategory == "" ? title: ", \(title)"
}
category = tempCategory
reviewCount = try container.decode(Int.self, forKey: .reviewCount)
coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
address = try container.decode(Address.self, forKey: .address)
isClaimed = try container.decode(Bool.self, forKey: .isClaimed)
isClosed = try container.decode(Bool.self, forKey: .isClosed)
url = try container.decode(URL.self, forKey: .url)
phone = try container.decode(String.self, forKey: .phone)
photoURLs = try container.decode([URL].self, forKey: .photoURLs)
hours = try container.decodeIfPresent([Hours].self, forKey: .hours)
}
}
Here is the json for a business list.
{
"businesses": [
{
"id": "I1D8NHvMWf8oMYceTyLlHg",
"name": "John Mills Himself",
"image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
"is_closed": false,
"url": "https://www.yelp.com/biz/john-mills-himself-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
"review_count": 55,
"categories": [
{
"alias": "coffee",
"title": "Coffee & Tea"
},
{
"alias": "bakeries",
"title": "Bakeries"
},
{
"alias": "wine_bars",
"title": "Wine Bars"
}
],
"rating": 4.5,
"coordinates": {
"latitude": -27.47151,
"longitude": 153.025654
},
"transactions": [],
"price": "$",
"location": {
"address1": "40 Charlotte St",
"address2": "",
"address3": "",
"city": "Brisbane",
"zip_code": "4000",
"country": "AU",
"state": "QLD",
"display_address": [
"40 Charlotte St",
"Brisbane Queensland 4000",
"Australia"
]
},
"phone": "",
"display_phone": "",
"distance": 383.2254392716822
},
{
"id": "KaIoCOg-IZJtLdN39Cw__Q",
"alias": "strauss-brisbane",
"name": "Strauss",
"image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/eYKx68uOaEY5k9Jt4TrQPw/o.jpg",
"is_closed": false,
"url": "https://www.yelp.com/biz/strauss-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
"review_count": 33,
"categories": [
{
"alias": "cafes",
"title": "Cafes"
}
],
"rating": 4.5,
"coordinates": {
"latitude": -27.469804,
"longitude": 153.027384
},
"transactions": [],
"price": "$",
"location": {
"address1": "189 Elizabeth St",
"address2": "",
"address3": "",
"city": "Brisbane",
"zip_code": "4000",
"country": "AU",
"state": "QLD",
"display_address": [
"189 Elizabeth St",
"Brisbane Queensland 4000",
"Australia"
]
},
"phone": "+61732365232",
"display_phone": "+61 7 3236 5232",
"distance": 247.3760157828213
}
}
And this is json response for individual BusinessDetail.
{
"id": "I1D8NHvMWf8oMYceTyLlHg",
"name": "John Mills Himself",
"image_url": "https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
"is_claimed": false,
"is_closed": false,
"url": "https://www.yelp.com/biz/john-mills-himself-brisbane?adjust_creative=0mCaOEYvfM9_oOaXgMuW6A&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_lookup&utm_source=0mCaOEYvfM9_oOaXgMuW6A",
"phone": "",
"display_phone": "",
"review_count": 55,
"categories": [
{
"alias": "coffee",
"title": "Coffee & Tea"
},
{
"alias": "bakeries",
"title": "Bakeries"
},
{
"alias": "wine_bars",
"title": "Wine Bars"
}
],
"rating": 4.5,
"location": {
"address1": "40 Charlotte St",
"address2": "",
"address3": "",
"city": "Brisbane",
"zip_code": "4000",
"country": "AU",
"state": "QLD",
"display_address": [
"40 Charlotte St",
"Brisbane Queensland 4000",
"Australia"
],
"cross_streets": ""
},
"coordinates": {
"latitude": -27.47151,
"longitude": 153.025654
},
"photos": [
"https://s3-media3.fl.yelpcdn.com/bphoto/OH84e6eP8zpzBECF0WvTXA/o.jpg",
"https://s3-media1.fl.yelpcdn.com/bphoto/L8dJYv1GCW1m5IvD0M3qXw/o.jpg",
"https://s3-media4.fl.yelpcdn.com/bphoto/oUH4cJmPRuAs_jv_xRtXQg/o.jpg"
],
"price": "$",
"hours": [
{
"open": [
{
"is_overnight": false,
"start": "0630",
"end": "1530",
"day": 0
},
{
"is_overnight": false,
"start": "0630",
"end": "1530",
"day": 1
},
{
"is_overnight": false,
"start": "1600",
"end": "2100",
"day": 1
},
{
"is_overnight": false,
"start": "0630",
"end": "1530",
"day": 2
},
{
"is_overnight": false,
"start": "1600",
"end": "2100",
"day": 2
},
{
"is_overnight": false,
"start": "0630",
"end": "1530",
"day": 3
},
{
"is_overnight": false,
"start": "1600",
"end": "2330",
"day": 3
},
{
"is_overnight": false,
"start": "0630",
"end": "1530",
"day": 4
},
{
"is_overnight": false,
"start": "1600",
"end": "2330",
"day": 4
},
{
"is_overnight": false,
"start": "1600",
"end": "2330",
"day": 5
}
],
"hours_type": "REGULAR",
"is_open_now": true
}
]
}
As you can see, BusinessDetail has exactly same info as Business at the top and I'd like to change BusinessDetail like below.
struct BusinessDetail {
let business: Business
let isClaimed: Bool
let isClosed: Bool
let url: URL
let phone: String?
let photoURLs: [URL]
var hours: [Hours]?
}
However, I am not sure if it is technically possible.

Here's a conceptual example, but you can apply the same principle to your case, with the following JSON:
{ "a": 1, "b": 2, "c": "three" }
and these models:
struct Smaller: Decodable {
var a, b: Int
}
struct Larger: Decodable {
var smaller: Smaller
var c: String
}
Decoding Smaller doesn't need any extra work (i.e. no need to manually implement the init(from: Decoder) initializer):
let decoder = JSONDecoder()
let small = try! decoder.decode(Smaller.self, from: jsonData)
But with Larger, you need to manually do this:
struct Larger: Decodable {
var smaller: Smaller
var c: String
enum CodingKeys: CodingKey {
case c
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.smaller = try Smaller.init(from: decoder)
self.c = try container.decode(String.self, forKey: .c)
}
}

Related

How to pass a value while parsing a nested JSON in Swift

I would like to pass the parent area Id to it's children areas while parsing the nested JSON structure as per the attached response, Here I would like to insert 'parentId' for each children which will link to it's immediate parent area,
{
"areas": [
{
"id": "271341877549072423",
"name": "Breeze Office Tower",
"children": [
{
"id": "271341877549072424",
"name": "100 flinders street",
"position": 0,
"children": []
},
{
"id": "271341877549130929",
"name": "100 flinders street",
"position": 1,
"children": []
},
{
"id": "271341877549072425",
"name": "100 Flinder Stree",
"position": 2,
"children": [
{
"id": "271341877549072426",
"name": "Büro",
"position": 0,
"children": [
{
"id": "271341877549072427",
"name": "Dachgeschoß",
"position": 0,
"children": []
}
]
}
]
},
{
"id": "271341877549130931",
"name": "100 Flinder Stree",
"position": 3,
"children": [
{
"id": "271341877549130933",
"name": "Büro",
"position": 0,
"children": [
{
"id": "271341877549130935",
"name": "Dachgeschoß",
"position": 0,
"children": []
}
]
}
]
}
]
}
]
}
My JSON Codable model struct looks like,
struct AreaModel: Decodable {
var areas: [NestedAreaModel]?
}
struct NestedAreaModel: Codable {
let areaId: String
let areaName: String
let children: [NestedAreaModel]
let hasChildren: Bool
var areaPosition: Int16?
var parentId: String?
var projectId: String?
enum CodingKeys: String, CodingKey {
case areaId = "id"
case areaName = "name"
case areaPosition = "position"
case children
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.areaId = try values.decode(String.self, forKey: .areaId)
self.children = try values.decode([NestedAreaModel].self, forKey: .children)
self.areaName = try values.decode(String.self, forKey: .areaName)
self.projectId = ORAUserDefaults.selectedProjectId()
self.areaPosition = try values.decodeIfPresent(Int16.self, forKey: .areaPosition)
if !self.children.isEmpty {
self.hasChildren = true
self.parentId = self.areaId
} else {
self.hasChildren = false
}
}
}
Here I am not able to set the parent Id, its pointing its own id always.
As I allready pointed out in the comments you would need to iterate over the decoded children and set their respective parentId to the current areaId.
One possible Solution would be:
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.areaId = try values.decode(String.self, forKey: .areaId)
// decode the children to a local var
var children = try values.decode([NestedAreaModel].self, forKey: .children)
self.areaName = try values.decode(String.self, forKey: .areaName)
self.projectId = ORAUserDefaults.selectedProjectId()
self.areaPosition = try values.decodeIfPresent(Int16.self, forKey: .areaPosition)
// if there are children loop over them and assign your id
if !children.isEmpty {
self.hasChildren = true
for (index, _ ) in children.enumerated(){
children[index].parentId = areaId
}
} else {
self.hasChildren = false
}
// assign to self
self.children = children
}
Tested with the given JSON:
let decoded = try JSONDecoder().decode(AreaModel.self, from: data)
print(decoded.areas?[0].areaId)
print(decoded.areas?[0].children[0].parentId)
Result:
Optional("271341877549072423")
Optional("271341877549072423")
Remarks:
Regarding your comment on complexity:
children[index].parentId = areaId runs exactly once per child no matter of the level deapth. So this function is (O)n.

How to save multiple object in single UserDefaults

I've created NSObject class for "CurrentUser",
here, sign-up API will call & response data will insert into the "CurrentUser" model.
This is one type of global model, like in any screen user_logic_details will fetch, modify & save.
Example :
let loginuser : CurrentUser = CurrentUser.getLoginData()!
loginuser.email = "abc#gmail.com"
CurrentUser.saveLoginData(loginData: loginuser)
// working
let email = CurrentUser.getLoginData()?.email ?? "" // i'll get "abc#gmail.com"
in the above example if I write the below code then it shows nil data
let loginuser : CurrentUser = CurrentUser.getLoginData()!
loginuser.email = "abc#gmail.com"
CurrentUser.saveLoginData(loginData: loginuser)
// working
let email = CurrentUser.getLoginData()?.email ?? "" // i'll get "abc#gmail.com"
// not working
let roleName = CurrentUser.getLoginData()?.roles?.name ?? "" // showing get ""
I'm not able to find exact issues here,
Check below the code of how I use the model class, saving data into UserDefaults & retrieve data from the model.
class CurrentUser: NSObject, NSCoding, NSKeyedUnarchiverDelegate {
var email : String!
var roles : UserRole!
private var _isLoggedIn = false
required convenience init(coder aDecoder: NSCoder) {
self.init()
roles = aDecoder.decodeObject(forKey: "roles") as? UserRole
email = aDecoder.decodeObject(forKey: "email") as? String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(email, forKey: "email")
aCoder.encode(roles, forKey: "roles")
}
class func getLoginData() -> CurrentUser? {
let userDefaults = UserDefaults.standard
if let UserData = userDefaults.object(forKey: "loginUser") {
guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(UserData as! Data)
else {
return nil
}
return unarchivedFavorites as? CurrentUser
} else {
return CurrentUser()
}
}
class func saveLoginData(loginData: CurrentUser?) {
do {
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: loginData as Any, requiringSecureCoding: false)
let userDefaults: UserDefaults = UserDefaults.standard
userDefaults.set(encodedData, forKey: "loginUser")
userDefaults.synchronize()
} catch {
print("Couldn't write file")
}
}
func unarchiver(_ unarchiver: NSKeyedUnarchiver, cannotDecodeObjectOfClassName name: String, originalClasses classNames: [String]) -> AnyClass? {
print("classNames", classNames)
return nil
}
}
class UserRole: NSObject, NSCoding {
var name : String!
required convenience init(coder aDecoder: NSCoder) {
self.init()
name = aDecoder.decodeObject(forKey: "name") as? String
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
}
}
The above model is for reference purposes.
I need to convert the below Response to the whole model class
{
"addressList": [
{
"addressLabel": "string",
"city": "string",
"country": "string",
"createdAt": "2022-03-16T12:10:41.148Z",
"homePhone": "string",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isDefault": true,
"isDeleted": true,
"state": "string",
"streetAddress": "string",
"updatedAt": "2022-03-16T12:10:41.148Z",
"version": 0,
"zip": "string"
}
],
"ageRange": "string",
"badges": [
{
"badge": {
"createdAt": "2022-03-16T12:10:41.148Z",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"image": "string",
"isDeleted": true,
"label": "string",
"name": "string",
"status": "ACTIVE",
"updatedAt": "2022-03-16T12:10:41.148Z",
"users": [
null
],
"version": 0
},
"userBadgeId": {
"badgeId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
],
"classesTaken": 0,
"createdAt": "2022-03-16T12:10:41.148Z",
"deviceList": [
{
"createdAt": "2022-03-16T12:10:41.148Z",
"deviceId": "string",
"deviceName": "string",
"deviceType": "ANDROID",
"display": "string",
"fcmToken": "string",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isDeleted": true,
"osVersion": "string",
"updatedAt": "2022-03-16T12:10:41.148Z",
"version": 0
}
],
"displayName": "string",
"displayPic": "string",
"dob": "2022-03-16T12:10:41.148Z",
"email": "string",
"experience": 0,
"firebaseToken": "string",
"firebaseUserId": "string",
"followerCount0l": 0,
"fullName": "string",
"gender": "FEMALE",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isDeleted": true,
"lastLoginTime": "2022-03-16T12:10:41.148Z",
"location": "string",
"phoneNumber": "string",
"roles": [
{
"createdAt": "2022-03-16T12:10:41.148Z",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"isDeleted": true,
"name": "ROLE_ADMIN",
"updatedAt": "2022-03-16T12:10:41.148Z",
"version": 0
}
],
"rubyBalance": 0,
"signupDateTime": "2022-03-16T12:10:41.148Z",
"signupSource": {
"buildNumber": "string",
"deviceId": "string",
"deviceType": "ANDROID",
"osVersion": "string"
},
"status": "ACTIVE",
"studentsTaught": 0,
"updatedAt": "2022-03-16T12:10:41.148Z",
"userInterest": [
"string"
],
"userName": "string",
"version": 0
}
If anyone has a better way to do it or a better solution, please answer it.
Your code reads like Objective-C from a few years ago. Something more modern and Swift-like might be:
#propertyWrapper
struct UserDefaultsCodableWrapper<T: Codable> {
let key: String
let defaultValue: T?
init(_ key: String, defaultValue: T?) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T? {
get {
guard let data = UserDefaults.standard.data(forKey: key),
let decoded = try? JSONDecoder().decode(T.self, from: data)
else { return defaultValue }
return decoded
}
set {
let encoded = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
struct User: Codable {
var email: String // TODO make a codable "Email" struct type that validates its input, no invalid strings allowed
var roles: [UserRole]
}
struct UserRole: Codable {
var name: String
}
class AppState {
#UserDefaultsCodableWrapper("CurrentUser", defaultValue: nil)
var currentUser: User?
}
class ViewController: UIViewController {
let state = AppState()
override func viewDidLoad() {
super.viewDidLoad()
print(String(describing: state.currentUser)) // nil
state.currentUser = User(email: "foo#bar.baz", roles: [.init(name: "Foo"), .init(name: "Bar")])
print(String(describing: state.currentUser)) // Optional(User...
Also worth pointing you to app.quicktype.io which can generate you codable models from your json.

Expected to decode Dictionary

Response
[
{
"result": "success",
"category": [
{
"categoryID": "1",
"category_name": "Health",
"category_image": "1573456796.jpg",
"about": "Start your health journey with scientifically developed, guided meditations across below listed verticals",
"color": "",
"special": "N",
"tags": "fitness|energy|strength",
"is_active": "Y",
"description": "Explore guided health meditations to build stronger emotional, mental & physical health",
"added_on": "2019-11-22 03:21:36",
"no_items": "7"
},
{
"categoryID": "2",
"category_name": "Work",
"category_image": "1556794807.jpg",
"about": "Accelerate your work life with scientifically developed, guided meditations across below listed verticals ",
"color": "",
"special": "N",
"tags": null,
"is_active": "Y",
"description": "Explore guided work meditations to release stress, foster progressive mindset & excel at work.",
"added_on": "2019-05-02 04:30:07",
"no_items": "2"
},
{
"categoryID": "3",
"category_name": "Relationships",
"category_image": "1556794814.jpg",
"about": "Illuminate your relations with scientifically developed, guided meditations across below listed verticals",
"color": "",
"special": "N",
"tags": null,
"is_active": "Y",
"description": "Explore guided relationship meditations to create a deep & loving bond with the self and others",
"added_on": "2019-05-02 04:30:14",
"no_items": "0"
},
{
"categoryID": "4",
"category_name": "Mindfulness",
"category_image": "1556794819.jpg",
"about": "Rewire your brain & turn your NOW into WOW™ with scientifically developed, guided mindfulness meditations",
"color": "",
"special": "N",
"tags": null,
"is_active": "Y",
"description": "Explore guided mindfulness meditations to develop focus & awareness in order to turn your NOW into WOW™",
"added_on": "2019-05-02 04:30:19",
"no_items": "0"
},
{
"categoryID": "5",
"category_name": "Students",
"category_image": "1556794824.jpg",
"about": "Spark up your student life with scientifically developed, guided meditations across below listed verticals ",
"color": "",
"special": "N",
"tags": null,
"is_active": "Y",
"description": "Explore guided student meditations to strengthen mind power in order to emerge as a super student",
"added_on": "2019-05-02 04:30:24",
"no_items": "0"
},
{
"categoryID": "6",
"category_name": "Affirmations",
"category_image": "1556794832.jpg",
"about": "Prime your mind, body & emotions with scientifically developed, affirmations across below listed verticals.",
"color": "",
"special": "N",
"tags": "tags|testing|search",
"is_active": "Y",
"description": "Explore guided affirmations to train your mind & body to unleash the infinite potential within",
"added_on": "2019-05-02 04:30:32",
"no_items": "0"
}
]
}
]
Error
typeMismatch(Swift.Dictionary,
Swift.DecodingError.Context(codingPath: [], debugDescription:
"Expected to decode Dictionary but found an array
instead.", underlyingError: nil))
Implementation
import Foundation
import Alamofire
struct Library : Codable {
let result : String?
let category : [Category]?
enum CodingKeys: String, CodingKey {
case result = "result"
case category = "category"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
result = try values.decodeIfPresent(String.self, forKey: .result)
category = try values.decodeIfPresent([Category].self, forKey: .category)
}
}
struct Category : Codable {
let categoryID : String?
let category_name : String?
let category_image : String?
let about : String?
let color : String?
let special : String?
let tags : String?
let is_active : String?
let description : String?
let added_on : String?
let no_items : String?
enum CodingKeys: String, CodingKey {
case categoryID = "categoryID"
case category_name = "category_name"
case category_image = "category_image"
case about = "about"
case color = "color"
case special = "special"
case tags = "tags"
case is_active = "is_active"
case description = "description"
case added_on = "added_on"
case no_items = "no_items"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
categoryID = try values.decodeIfPresent(String.self, forKey: .categoryID)
category_name = try values.decodeIfPresent(String.self, forKey: .category_name)
category_image = try values.decodeIfPresent(String.self, forKey: .category_image)
about = try values.decodeIfPresent(String.self, forKey: .about)
color = try values.decodeIfPresent(String.self, forKey: .color)
special = try values.decodeIfPresent(String.self, forKey: .special)
tags = try values.decodeIfPresent(String.self, forKey: .tags)
is_active = try values.decodeIfPresent(String.self, forKey: .is_active)
description = try values.decodeIfPresent(String.self, forKey: .description)
added_on = try values.decodeIfPresent(String.self, forKey: .added_on)
no_items = try values.decodeIfPresent(String.self, forKey: .no_items)
}
}
extension Library {
init(data: Data) throws {
let decoder = JSONDecoder()
self = try decoder.decode(Library.self, from: data)
}
init(_ json: String, using encoding: String.Encoding = .utf8) throws {
guard let data = json.data(using: encoding) else {
throw NSError(domain: "JSONDecoding", code: 0, userInfo: nil)
}
try self.init(data: data)
}
init(fromURL url: URL) throws {
try self.init(data: try Data(contentsOf: url))
}
func jsonData() throws -> Data {
return try JSONEncoder().encode(self)
}
func jsonString(encoding: String.Encoding = .utf8) throws -> String? {
return String(data: try self.jsonData(), encoding: encoding)
}
}
// MARK: - Alamofire response handlers
extension DataRequest {
fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
print(response)
guard error == nil else { return .failure(error!) }
guard let data = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}
return Result { try JSONDecoder().decode(T.self, from: data) }
}
}
#discardableResult
fileprivate func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<T>) -> Void) -> Self {
return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler)
}
#discardableResult
func responsePhoto(queue: DispatchQueue? = nil, completionHandler: #escaping (DataResponse<Library>) -> Void) -> Self {
return responseDecodable(queue: queue, completionHandler: completionHandler)
}
}
you cannot assign the decode to self because it different type I mean self means Library but what we are decoding is array of Library so .
init(data: Data) throws {
let decoder = JSONDecoder()
self = try decoder.decode(Library.self, from: data)
}
Example I have taken response to string for easy parsing
var stringText = """
[
{
"result": "success",
"category": [
{
"categoryID": "1",
"category_name": "Health",
"category_image": "1573456796.jpg",
"about": "Start your health journey with scientifically developed, guided meditations across below listed verticals",
"color": "",
"special": "N",
"tags": "fitness|energy|strength",
"is_active": "Y",
"description": "Explore guided health meditations to build stronger emotional, mental & physical health",
"added_on": "2019-11-22 03:21:36",
"no_items": "7"
}
]
}
]
"""
let jsonData = Data(responceString.utf8)
let decoder = JSONDecoder()
do {
let responceData = try decoder.decode([Library].self, from: jsonData)
print(responceData)
} catch {
print(error.localizedDescription)
}
write
return Result { try JSONDecoder().decode([T].self, from: data) }
instead of
return Result { try JSONDecoder().decode(T.self, from: data) }

How do I use Codable to decode json response which has changing keys?

I have a json response from the fixer api. And I want to decode it to the struct which is shown below. The issue is the values of "rates" in the json is an array of key:value pairs, which has varying keys.
I have the second structure which must be mapped from these key-values.
How can i do it using Codable?
{
"success": true,
"timestamp": 1558883585,
"base": "EUR",
"date": "2019-05-26",
"rates": {
"AED": 4.116371,
"AFN": 90.160103,
"ALL": 122.341655,
"AMD": 536.359254,
"ANG": 2.097299,
"AOA": 368.543773,
"ARS": 50.418429,
"AUD": 1.618263,
"AWG": 2.012124,
"AZN": 1.910752,
"BAM": 1.955812,
"BBD": 2.227793,
"BDT": 94.245988,
"BGN": 1.956097,
"BHD": 0.421705,
"BIF": 2051.459244,
"BMD": 1.120649,
"BND": 1.539664,
"BOB": 7.729394,
"BRL": 4.508038,
"BSD": 1.118587,
"BTC": 0.00014,
"BTN": 77.755286,
"BWP": 12.040813,
"BYN": 2.323273,
"BYR": 21964.71167,
"BZD": 2.254689
}
}
If i change the 'ExchangeRate' struct as below, it is easily decoded. But I have the requirement where the "rates" should be an array of "ConvertedRate" struct.
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [String:Double]
}
This is what i need.
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [ConvertedRate] //keys are varied in json
}
struct ConvertedRate : Codable, Comparable{
let currencyName : String
let rate : Double
}
You'll have to write a custom init(from:) since you are not using the "default behavior".
More info on the Apple documentation.
Here is a possible solution:
struct ExchangeRate : Codable {
let base : String
let date : String
let rates : [ConvertedRate]
enum CodingKeys: String, CodingKey {
case base
case date
case rates
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
base = try values.decode(String.self, forKey: .base)
date = try values.decode(String.self, forKey: .date)
let additionalInfo = try values.decode([String: Double].self, forKey: .rates)
print(additionalInfo)
rates = additionalInfo.map({ ConvertedRate(name: $0.key, rate: $0.value) })
}
}
struct ConvertedRate : Codable {
let currencyName : String
let rate : Double
init(name: String, rate: Double) {
self.currencyName = name
self.rate = rate
}
}
Side Note: The current code can decode your JSON as you want, but it doesn't re-encoded as it was, because it's replicating the Swift structure:
{
"base": "EUR",
"date": "2019-05-26",
"rates": [{
"currencyName": "BSD",
"rate": 1.118587
}, {
"currencyName": "BWP",
"rate": 12.040813
}, {
"currencyName": "BYN",
"rate": 2.3232729999999999
}, {
"currencyName": "BBD",
"rate": 2.2277930000000001
}, {
"currencyName": "BOB",
"rate": 7.7293940000000001
}, {
"currencyName": "BAM",
"rate": 1.9558120000000001
}, {
"currencyName": "AUD",
"rate": 1.618263
}, {
"currencyName": "AFN",
"rate": 90.160103000000007
}, {
"currencyName": "BYR",
"rate": 21964.711670000001
}, {
"currencyName": "BRL",
"rate": 4.508038
}, {
"currencyName": "BMD",
"rate": 1.120649
}, {
"currencyName": "BGN",
"rate": 1.956097
}, {
"currencyName": "BHD",
"rate": 0.421705
}, {
"currencyName": "ANG",
"rate": 2.097299
}, {
"currencyName": "AOA",
"rate": 368.54377299999999
}, {
"currencyName": "BZD",
"rate": 2.2546889999999999
}, {
"currencyName": "ARS",
"rate": 50.418429000000003
}, {
"currencyName": "BTC",
"rate": 0.00013999999999999999
}, {
"currencyName": "BIF",
"rate": 2051.4592440000001
}, {
"currencyName": "AWG",
"rate": 2.012124
}, {
"currencyName": "AED",
"rate": 4.116371
}, {
"currencyName": "AMD",
"rate": 536.35925399999996
}, {
"currencyName": "BDT",
"rate": 94.245987999999997
}, {
"currencyName": "BND",
"rate": 1.5396639999999999
}, {
"currencyName": "BTN",
"rate": 77.755285999999998
}, {
"currencyName": "AZN",
"rate": 1.910752
}, {
"currencyName": "ALL",
"rate": 122.341655
}]
}
You have to write a custom initializer which maps the dictionary key-value pairs to an array of the custom struct.
It's not necessary to adopt Codable in CurrencyRate because the instances are created manually
let jsonString = """
{
"success": true,
"timestamp": 1558883585,
"base": "EUR",
"date": "2019-05-26",
"rates": {
"AED": 4.116371,"AFN": 90.160103,"ALL": 122.341655,"AMD": 536.359254,"ANG": 2.097299,"AOA": 368.543773,"ARS": 50.418429,"AUD": 1.618263,"AWG": 2.012124,"AZN": 1.910752,"BAM": 1.955812,"BBD": 2.227793,"BDT": 94.245988,"BGN": 1.956097,"BHD": 0.421705,"BIF": 2051.459244,"BMD": 1.120649,"BND": 1.539664,"BOB": 7.729394,"BRL": 4.508038,"BSD": 1.118587,"BTC": 0.00014,"BTN": 77.755286,"BWP": 12.040813,"BYN": 2.323273,"BYR": 21964.71167,"BZD": 2.254689
}
}
"""
struct ExchangeData : Codable {
let timestamp : Date
let base : String
let date : String
let rates : [CurrencyRate]
private enum CodingKeys: String, CodingKey { case timestamp, base, date, rates}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
timestamp = try container.decode(Date.self, forKey: .timestamp)
base = try container.decode(String.self, forKey: .base)
date = try container.decode(String.self, forKey: .date)
let rateData = try container.decode([String:Double].self, forKey: .rates)
rates = rateData.map(CurrencyRate.init)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(timestamp, forKey: .timestamp)
try container.encode(base, forKey: .base)
try container.encode(date, forKey: .date)
var rateData = [String:Double]()
rates.forEach{ rateData[$0.name] = $0.rate }
try container.encode(rateData, forKey: .rates)
}
}
struct CurrencyRate {
let name : String
let rate : Double
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let result = try decoder.decode(ExchangeData.self, from: data)
print(result)
} catch {
print(error)
}

How to use Decodable with Generics & Core data in Swift

Below is my JSON. I need to decode the entries in it using "Decodable" CoreData model class. The Entries(and corresponding model classes in Core data) are different as you can see against key "c". So this variable "c" needs to be of type generic i guess. So it can accept all 6(fixed!) types of models viz. "consultation_time", "child", "appointment", "child_doctor", "clinic", "clinic_user". Not sure how to implement this .
Model classes generated by core data.(Have used https://forums.developer.apple.com/thread/96860#295158 this link to implement Decodable with Coredata.)
Updates+CoreDataClass.swift
import Foundation
import CoreData
public class Updates: NSManagedObject {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name ?? "" , forKey: .name)
try container.encode(ids , forKey: .ids)
try container.encode(c , forKey: .c)
try container.encode(u , forKey: .u)
try container.encode(d , forKey: .d)
}
public required convenience init(from decoder: Decoder) throws {
guard let contextUserInfoKey = CodingUserInfoKey.context,
let managedObjectContext = decoder.userInfo[contextUserInfoKey] as? NSManagedObjectContext,
let entity = NSEntityDescription.entity(forEntityName:"Sync", in: managedObjectContext) else { fatalError() }
self.init(entity: entity, insertInto: managedObjectContext)
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
ids = try values.decode(Array?.self, forKey: .ids)
c = try values.decode(Array.self, forKey: .c)
u = try values.decode(Array.self, forKey: .u)
d = try values.decode(Array.self, forKey: .d)
}
}
Updates+CoreDataProperties.swift
import Foundation
import CoreData
extension Updates : Codable {
#nonobjc public class func fetchRequest() -> NSFetchRequest<Updates>
{
return NSFetchRequest<Updates>(entityName: "Updates")
}
#NSManaged public var name: String?
#NSManaged public var ids: [String]?
#NSManaged public var c: [Parent]?
#NSManaged public var u: [Parent]?
#NSManaged public var d: [Parent]?
//** Here Parent is another Coredata entity model class. And sub entities are created from it. So at runtime c, u & d can have any of values amongst the sub-entities(Child, Clinic, Appointment etc.). So what happens is while decoding c, u and d it tries to decode "Parent" and not the subentities , becoz it doesnt know exact type of subentity(Child, Clinic, Appointment etc.). It just knows it as "Parent".
enum CodingKeys: String, CodingKey {
case name
case ids = "sync_token"
case c
case u
case d
}
}
JSON :
"updates": [
{
"name": "consultation_time",
"ids": [ "id" ],
"c": [
{
"id": 1,
"_id": 1001,
"is_active_morning": true,
"morning_start_time": "2016-07-01T09:30:00.000+05:30",
"morning_end_time": "2016-07-01T12:30:00.000+05:30",
"is_active_evening": true,
"evening_start_time": "2016-07-01T18:00:00.000+05:30",
"evening_end_time": "2016-07-01T19:00:00.000+05:30",
"day": "Monday",
"doctor_id": 2,
"clinic_id": 1
}
],
"u": [],
"d": []
},
{
"name": "child",
"ids": [ "id" ],
"c": [
{
"id": 1,
"_id": 1022,
"name": "Sarvesh",
"gender": "male",
"dob": "2017-01-01T17:48:20.393+05:30",
"profile_image": "https://tpn-production.s3.amazonaws.com/710e34ae-49bb-11e7-99dc- 0a6eb743b653.jpg",
"is_member": true,
"email": "kiran#joshsoftware.com",
"birthday_wish_sms": true,
"program_launch_sms": true,
"vaccine_reminder_same_day_sms": true,
"vaccine_reminder_two_days_prior_sms": true,
"vaccine_reminder_week_prior_sms": true,
"parent_name": "Kiran"
}
],
"u": [],
"d": []
},
{
"name": "appointment",
"ids": [ "id" ],
"c": [
{
"id": 1,
"_id": 1076,
"start_time": "2016-07-01T19:10:00.000+05:30",
"end_time": "2016-07-01T19:15:00.000+05:30",
"schedule": "Evening",
"status": "open",
"weight": 9.2,
"height": 45,
"head_circumference": null,
"temperature": null,
"heart_rate": null,
"blood_pressure": null,
"respiratory_rate": null,
"bill_amount": null,
"child_id": 1,
"doctor_id": 2,
"clinic_id": 1,
"amount_received_for_consultation": null,
"amount_received_for_indoor_admission": null,
"amount_received_for_nebulization": null,
"amount_received_for_injection": null,
"amount_received_for_vaccination": null,
"payment_mode": null,
"total_amount_received": null,
"pending_amount_till_date": 100
}
],
"u": [],
"d": []
},
{
"name": "child_doctor",
"ids": [ "id" ],
"c": [
{
"id": 129089,
"_id": null,
"child_id": 12617,
"doctor_id": 2,
"clinic_id": 1
}
],
"u": [],
"d": []
},
{
"name": "clinic",
"ids": [ "id" ],
"c": [
{
"id": 1,
"_id": 2701,
"name": "Wellness Clinic",
"landline": "02027654321",
"address": {
"address": "A13/14/15, Sunflower Apartments",
"area": "Baner",
"postal_code": "411045",
"city": "Pune",
"state": "MH",
"country": "IN",
"lat": "18.561338",
"lng": "73.785545"
},
"mobile": null,
"email": null
}
],
"u": [],
"d": []
},
{
"name": "clinics_user",
"ids": [
"id"
],
"c": [
{
"id": 1,
"clinic_id": 1,
"is_active": true,
"appointment_time_slot": 5,
"consultation_rate": 400,
"block_online_appointments_morning": false,
"block_online_appointments_evening": false,
"show_generic_name_first": false,
"show_scheduled_vaccine": true,
"show_regional_translation_of_number": false,
"doctor_id": 2
}
],
"u": [],
"d": []
}
]

Resources