How to use Codable encode with object inside object - ios

Im working on a JSON payload with my object payload. but im having trouble encoding object inside object.
My Payload class was this
class ConversationPayload :BaseObject {
var title : String? = ""
var messageDict: MessagePayload = MessagePayload()
var participants: [Int32] = []
var type: String = ""
enum CodingKeys: String, CodingKey {
case title = "title"
case messageDict = "message"
case participants = "participants"
case type = "type"
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if title != nil {
try container.encode(title, forKey: .title)
}
try container.encode(messageDict, forKey: .messageDict)
try container.encode(participants, forKey: .participants)
try container.encode(type, forKey: .type)
}
}
class MessagePayload: BaseObject {
var body : String = ""
var isVideocallInvite: Bool = false
var attachmentsPayload: MessageAttachmentPayload? = nil
enum CodingKeys: String, CodingKey {
case body = "body"
case isVideocallInvite = "is_videocall_invite"
case attachmentsPayload = "attachment"
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(body, forKey: .body)
try container.encode(isVideocallInvite, forKey: .isVideocallInvite)
if attachmentsPayload != nil {
try container.encode(attachmentsPayload, forKey: .attachmentsPayload)
}
}
}
class MessageAttachmentPayload: BaseObject {
var photo : String = ""
var photoType : String = "jpg"
}
BaseObject was this
class BaseObject:Codable{}
What i want to get in json payload was something like this
{"message": {"body": "body_string", "is_videocall_invite": 1}, "participants" : [user-id], "type" : "converstation_type","title":"title"}
anyone know whats wrong with my payload class? not that familiar yet on codable. thanks in advance.

I'm not sure what constrains you have here, but I would simplify all of this down. Keep the JSON data models as close to the JSON as you can get.
struct ConversationJsonModel: Codable {
var title: String?
var message: MessageJsonModel
var participants: [Int]
var type: String
}
struct MessageJsonModel: Codable {
var body: String
var is_videocall_invite: Int
var attachment: AttachmentJsonModel?
}
struct AttachmentJsonModel: Codable {
var photo: String
var photo_type: String // <-- assuming photo_type is the JSON member name.
}
If you need a view model or some other kind of local data model, then the two parts are separate.
class BaseObject {}
class ConversationPayload: BaseObject {
var title : String? = ""
var messageDict: MessagePayload = MessagePayload()
var participants: [Int32] = []
var type: String = ""
func makeConversationJsonModel() -> ConversationJsonModel {
ConversationJsonModel(title: title,
message: messageDict.makeMessageJsonModel(),
participants: participants.map { Int($0) },
type: type)
}
}
class MessagePayload: BaseObject {
var body : String = ""
var isVideocallInvite: Bool = false
var attachmentsPayload: MessageAttachmentPayload? = nil
func makeMessageJsonModel() -> MessageJsonModel {
MessageJsonModel(body: body,
is_videocall_invite: isVideocallInvite ? 1 : 0,
attachment: attachmentsPayload?.makeAttachmentJsonModel())
}
}
class MessageAttachmentPayload: BaseObject {
var photo : String = ""
var photoType : String = "jpg"
func makeAttachmentJsonModel() -> AttachmentJsonModel {
AttachmentJsonModel(photo: photo, photo_type: photoType)
}
}
Finally, encoding your JSON
let conversationPayload = ConversationPayload()
let json = try? JSONEncoder().encode(conversationPayload.makeConversationJsonModel())
This allows for clean separation between the JSON representation and the payload model. For example, in the JSON, is_videocall_invite is an Int (0 or 1); meanwhile, in the payload model, isVideocallInvite is a Bool.

What is the issue? I just tested your class, it has some minor issues but it does conforming to codable and it encoded without issue.
In playground:
var conversation = ConversationPayload()
var message = MessagePayload()
message.body = "message body"
message.isVideocallInvite = true
var attachment = MessageAttachmentPayload()
attachment.photo = "attachment_file"
message.attachmentsPayload = attachment
conversation.messageDict = message
conversation.title = "Title"
conversation.participants = [1, 2, 3, 4]
conversation.type = "Conversation Type"
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(conversation)
print(String(data: data, encoding: .utf8)!)
The output looks like the following:
{
"participants" : [
1,
2,
3,
4
],
"message" : {
"is_videocall_invite" : true,
"attachment" : {
"photo" : "attachment_file",
"photoType" : "jpg"
},
"body" : "message body"
},
"title" : "Title",
"type" : "Conversation Type"
}
I have added encode and CodingKey in MessageAttachmentPayload class to encode the values.
Isn't it what you were expecting?

Related

Managing Dynamic Keys in response through Codable Protocol

I need to make the codable model for the dynamic keys of dictionary coming from response below is the response I'm getting.
{
"data" : [
{
"desc1" : null,
"file1" : "uploads\/posts\/Aug-2021\/1629271422310452767"
},
{
"desc2" : "hello",
"file2" : "uploads\/posts\/Aug-2021\/162927142279356160WhatsApp+Image+2021-07-02+at+12.09.14+PM.jpeg"
}
],
"status" : "success"
}
This desc1 and file1 is dynamic till like file1, file2 and so on, I need to have codable model for that below is my model that is not supportive.
struct ListModel: Codable {
public var data: [data]?
}
struct data: Codable {
let file : String?
let desc : String?
}
Anything support by codable protocol for that. Thanks in Advance.
You need a custom initializer. Of course this will only work if your json will always come formatted as described:
struct File {
var file: String? = ""
var desc: String? = ""
}
struct Response {
let files: [File]
let status: String
enum CodingKeys: String, CodingKey {
case files = "data", status
}
}
extension Response: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.status = try container.decode(String.self, forKey: .status)
let elements = try container.decode([[String: String?]].self, forKey: .files)
self.files = elements.reduce(into: []) {
var file = File()
for (key, value) in $1 {
if key.hasPrefix("file") {
file.file = value
} else if key.hasPrefix("desc") {
file.desc = value
}
}
$0.append(file)
}
}
}
Playground testing:
let json = """
{
"data" : [
{
"desc1" : null,
"file1" : "uploads/posts/Aug-2021/1629271422310452767"
},
{
"desc2" : "hello",
"file2" : "uploads/posts/Aug-2021/162927142279356160WhatsApp+Image+2021-07-02+at+12.09.14+PM.jpeg"
}
],
"status" : "success"
}
"""
do {
let response = try JSONDecoder().decode(Response.self, from: Data(json.utf8))
print(response)
} catch {
print(error)
}
This will print:
Response(files: [File(file: Optional("uploads/posts/Aug-2021/1629271422310452767"), desc: nil), File(file: Optional("uploads/posts/Aug-2021/162927142279356160WhatsApp+Image+2021-07-02+at+12.09.14+PM.jpeg"), desc: Optional("hello"))], status: "success")

Swift Codable JSON parse error with JSONDecoder

I am trying to handle a JSON with Codable but there's a parsing error when I decode it with JSONDecoder().decode.
{
"data": [
{
"id": "90",
"symbol": "BTC",
"name": "Bitcoin",
"nameid": "bitcoin",
"rank": 1,
"price_usd": "50513.75",
"percent_change_24h": "3.03",
"percent_change_1h": "-0.50",
"percent_change_7d": "-9.91",
"price_btc": "1.00",
"market_cap_usd": "942710364520.73",
"volume24": 70745042591.75044,
"volume24a": 107034995571.4168,
"csupply": "18662452.00",
"tsupply": "18662452",
"msupply": "21000000"
},
{
"id": "80",
"symbol": "ETH",
"name": "Ethereum",
"nameid": "ethereum",
"rank": 2,
"price_usd": "4052.44",
"percent_change_24h": "10.17",
"percent_change_1h": "-0.78",
"percent_change_7d": "17.75",
"price_btc": "0.084812",
"market_cap_usd": "466734637594.73",
"volume24": 53134000887.50444,
"volume24a": 87082811090.79503,
"csupply": "115173595.00",
"tsupply": "115173595",
"msupply": ""
}
],
"info": {
"coins_num": 5949,
"time": 1621022046
}
}
The json is like above and I coded all my models like below.
class CoinModel: Codable {
var data: [Coin]?
var info: CoinInfo?
enum CodingKeys: String, CodingKey {
case data
case info
}
}
class CoinInfo: Codable {
var coinsNum: Int?
var time: TimeInterval?
enum CodingKeys: String, CodingKey {
case coinsNum = "coins_num"
case time = "time"
}
}
class Coin: Codable{
var csupply: String?
var id: String?
var marketCapUsd: String?
var msupply: Int?
var name: String?
var nameid: String?
var percentChange1h: String?
var percentChange24h: String?
var percentChange7d: String?
var priceBtc: String?
var priceUsd: String?
var rank: Int?
var symbol: String?
var tsupply: Int?
var volume24: Double?
var volume24a: Double?
enum CodingKeys: String, CodingKey {
case csupply = "csupply"
case id = "id"
case marketCapUsd = "market_cap_usd"
case msupply = "msupply"
case name = "name"
case nameid = "nameid"
case percentChange1h = "percent_change_1h"
case percentChange24h = "percent_change_24h"
case percentChange7d = "percent_change_7d"
case priceBtc = "price_btc"
case priceUsd = "price_usd"
case rank = "rank"
case symbol = "symbol"
case tsupply = "tsupply"
case volume24 = "volume24"
case volume24a = "volume24a"
}
}
And my network function work like below.
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
guard let data = data else {
completion(.failure(error ?? NSError()))
return
}
let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
guard let model = try? JSONDecoder().decode(CoinModel.self, from: data) else {
completion(.success([]))
return
}
completion(.success(model.data ?? []))
}.resume()
As I said before, json object is not nil and I got like below.
[({
csupply = "435032301.00";
id = 33285;
"market_cap_usd" = "435337535.24";
msupply = "";
name = "USD Coin";
nameid = "usd-coin";
"percent_change_1h" = "0.01";
"percent_change_24h" = "0.19";
"percent_change_7d" = "0.16";
"price_btc" = "0.000023";
"price_usd" = "1.00";
rank = 100;
symbol = USDC;
tsupply = 435032301;
volume24 = "11520659193.03477";
volume24a = "13684311331.83874";
}
)
, "info": {
"coins_num" = 5952;
time = 1621160044;
}]
I can handle the JSON with JSONSerialization but I could not find any reason for parse error in the JSONDecoder way. I think I did not see the problem. Thanks for any advice and helps.
Edit:
I solved it with several changes in my Codable class, I made a mistake when I describing model for id, csupply and etc.
var csupply: String?
var msupply: String?
var tsupply: String?
You have several mistakes on defining object. For example you have defined id, msupply is a integer but they are string. You have defined the volume24 and volume24a is string but they are not. You need to fix all of these probably thats why you can't parse it.
All wrong defined parameters are id, mSupply, volume24, volume24a, tSupply for coin object and for the info object you need to convert time to Int aswell.

swifty json parse array object

how can I set an array of Images to an images variable of the Car class
my json:
{
"Description": "test comment",
"GalleryId": 5548,
"ShortCode": "rzswig",
"Images": [
{
"Id": 9742,
"Link": "https://url/Images/5548/image9742_x560.jpg"
},
{
"Id": 9749,
"Link": "https://url/Images/5548/image9749_x560.jpg"
},
{
"Id": 9746,
"Link": "https://url/Images/5548/image9746_x560.jpg"
}
]
}
my class :
class Car: Hashable {
var shortCode: String
var description: String
var galleryId: Int
var imageItems: [ImageItem]?
init(response: JSON) {
shortCode = response["ShortCode"].stringValue
description = response["Description"].stringValue
galleryId = response["GalleryId"].intValue
imageItems = response["Images"].arrayObject.map {$0} as? [ImageItem]
}
init(shortCode: String, description: String, galleryId: Int, imageItems: [ImageItem]?) {
self.description = description
self.shortCode = shortCode
self.galleryId = galleryId
self.imageItems = imageItems
}
}
struct ImageItem {
var id: Int
var link: String
}
variant:
imageItems = response["Images"].arrayObject.map {$0} as? [ImageItem]
doesn't work for me
If you want to keep using SwiftyJSON, you can add an initialiser to ImageItem that takes a JSON, just like you did with Car:
init(response: JSON) {
id = response["Id"].intValue
link = response["Link"].stringValue
}
You might also want to add the autogenerated member wise initialiser back if you want it.
Then, to initialise imageItems, do:
imageItems = response["Images"].array?.map(ImageItem.init(response:))
This whole thing would be a lot easier if you used the Codable API built in to Swift.
Just conform both Car and ImageItem to Codable:
class Car: Codable {
var shortCode: String
var description: String
var galleryId: Int
var imageItems: [ImageItem]?
enum CodingKeys: String, CodingKey {
case shortCode = "ShortCode"
case description = "Description"
case galleryId = "GalleryId"
case imageItems = "Images"
}
}
struct ImageItem: Codable {
var id: Int
var link: String
enum CodingKeys: String, CodingKey {
case id = "Id"
case link = "Link"
}
}
And then do this to deserialise the json:
let car = JSONDecoder().decode(Car.self, data: jsonData) // jsonData is of type "Data"
I think the problem is that you map it as array of Image item
imageItems = response["Images"].arrayObject.map {$0} as? ImageItem
try response["images"].arrayValue

How to handle dynamic keys from a JSON response using jsonDecoder?

How do I handle this JSON and parse this JSON using decoder in Swift 4? I tried several times but failed. I don't understand how to handle this JSON format.
[
{
products: {
id: 69,
name: "test",
des: "Hi this is a test",
sort_des: "this is a test category",
},
documents: {
0: {
id: 1,
name: "105gg_1992uu",
citation: "This is citation for 105gg_1992uu",
file: "105gg_1992uu.pdf",
created_at: "2019-01-25 09:07:09",
category_id: 69,
},
1: {
id: 2,
name: "96tt-1997tt",
citation: "This is citation for 96tt-1997tt",
file: "96tt-1997tt.pdf",
created_at: "2019-01-25 09:07:09",
category_id: 69,
},
},
}
]
I tried the following code.
This is my model class.
struct GenericCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = "\(intValue)"
}
}
struct Model : Codable {
struct Documents : Codable {
var id : Int?
var name : String?
var citation : String?
var file : String?
var created_at : String?
var category_id : Int?## Heading ##
private enum CodingKeys : String, CodingKey{
case id = "id"
case name = "name"
case citation = "citation"
case file = "file"
case created_at = "created_at"
case category_id = "category_id"
}
}
struct Products : Codable {
var id : Int?
var name : String?
var des : String?
var sort_des : String?
private enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case des = "des"
case sort_des = "sort_des"
}
}
var products : Products?
var documents : [Documents]?
private enum CodingKeys : String, CodingKey{
case products
case documents
}
init(from decoder: Decoder) throws {
self.documents = [Documents]()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.products = try container.decode(Products.self, forKey: .products)
let documents = try container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .documents)
for doc in documents.allKeys{
let docEach = try documents.decode(Documents.self, forKey: doc)
self.documents?.append(docEach)
}
}
}
This is my fetch data from that JSON function
class LatestStatuesVC: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet var tableView: UITableView!
var caseData : [Model]?
var model : Model?
var countCaseData = 0
override func viewDidLoad() {
super.viewDidLoad()
downloadAllData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return countCaseData
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ReuseIdentifiers.cellLatestStatues, for: indexPath) as! LatestStatuesTableCell
return cell
}
//MARK: Download all documents into internal directory
func downloadAllData(){
let url = URL(string: URLString.urlForGetDocuments)
URLSession.shared.dataTask(with: url!) { (data, response, err) in
DispatchQueue.main.async {
do{
if err == nil {
let products = try JSONDecoder().decode(Model.Products.self, from: data!)
let documentAll = try JSONDecoder().decode([Model.Documents].self, from: data!)
print(products.name as Any)
self.countCaseData = documentAll.count
for doc in documentAll{
print(doc.name as Any)
print(doc.citation as Any)
}
}
}
catch let err{
print(err)
}
}
}.resume()
}
}
I get this error for this code.
typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))
The error clearly says that the root object of the JSON is an array but you try to decode a dictionary.
Basically your structs are too complicated. If you want to have documents as an array by getting rid of the numeric dictionary keys just write a custom initializer in the root (Model) struct which decodes documents as dictionary and takes the values sorted by id as the documents array.
struct Model : Decodable {
let products : Product
let documents : [Document]
enum CodingKeys: String, CodingKey { case products, documents }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
products = try container.decode(Product.self, forKey: .products)
let documentData = try container.decode([String:Document].self, forKey: .documents)
documents = documentData.values.sorted(by: {$0.id < $1.id})
}
}
struct Product: Decodable {
let id : Int
let name, description, sortDescription : String
let type : String
}
struct Document: Decodable {
let id, categoryId : Int
let name, citation, file : String
let createdAt : Date
}
Then decode the JSON (assuming data represents the JSON as Data), the createdAt values are decoded as Date
let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
decoder.dateDecodingStrategy = .formatted(dateFormatter)
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let modelArray = try decoder.decode([Model].self, from: data)
for model in modelArray {
print("products:",model.products)
print("documents:",model.documents)
}
} catch { print(error) }
The convertFromSnakeCase key decoding strategy converts all snake_cased keys to camelCased struct members without specifying any CodingKeys.

Mapping json response to custom object

I'm receiving following response from server.
[
{
"resourceEmail" : "",
"kind" : "admin#directory#resources#calendars#CalendarResource",
"resourceId" : "",
"resourceName" : "Inubation-Newton-4p",
"resourceType" : "Confroom",
"etags" : "\"eqO3c9wtJJ4wVWz2xe0E9HiU_D0\/rfmk2kZmhGi4HcHadds\""
},
{
"resourceEmail" : "...",
"kind" : "admin#directory#resources#calendars#CalendarResource",
"resourceId" : "...",
"resourceName" : "MVD11-R2-Napolitana",
"resourceType" : "1 Tel Room",
"etags" : "\"eqO3c9wtJJ4wVWz2xe0E9HiU_D0\/zcPwONBLID-O_3gvi0ly\""
},
]
How can I transform above json array to array of custom object. I'm using SwiftyJSON for creating custom object. Below is my custom object class.
class CalendarResource: NSObject {
var resourceID: String
var resourceEmail: String
var resourceName: String
var etags: String
init(json: JSON) {
self.resourceID = json[Constants.Model.CalendarResource.resourceID].stringValue
self.resourceEmail = json[Constants.Model.CalendarResource.resourceEmail].stringValue
self.resourceName = json[Constants.Model.CalendarResource.resourceName].stringValue
self.etags = json[Constants.Model.CalendarResource.etags].stringValue
}
}
Thanks
struct CalendarResource {
var resourceID: String
var resourceEmail: String
var resourceName: String
var etags: String
init(json: JSON) {
self.resourceID = json["resourceID"].stringValue
self.resourceEmail = json["resourceEmail"].stringValue
self.resourceName = json["resourceName"].stringValue
self.etags = json["etags"].stringValue
}
}
And when you are getting response for example like so:
//create array somewhere outside of the method, var calendarResourceArray: [CalendarResource]
let jsonArray = response
self.calendarResourceArray = jsonArray!.map{CalendarResource(json: $0)}

Resources