How to write the codable in generic format [duplicate] - swift

This question already has answers here:
What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?
(2 answers)
Closed 2 years ago.
I have understand how to make the codable wrapper class for services response structure.
But some times in server side the attribute value varies It may Int Or String.
Example
struct ResponseDataModel : Codable{
let data : DataClass?
enum CodingKey: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throw {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}
struct DataClass : Codable{
let id : Int
let name : String?
let age : Int?
enum CodingKey: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throw {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey:.it)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}
I would like to use generic way if id int string no matter what its it should bind to my controller with id value data.
let id : <T>
How to write the codable in generic formate.

You can do that using the following model:
struct ResponseDataModel<T: Codable>: Codable{
let data : DataClass<T>?
enum CodingKeys: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass<T>.self, forKey:.data)
}
}
struct DataClass<T: Codable>: Codable {
let id: T?
let name: String?
let age: Int?
enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(T.self, forKey:.id)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}
However, you should always known the type of the id property when you call decode(_:from:) function of JSONDecoder like this:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(ResponseDataModel<Int>.self, from: data)
print(decoded)
} catch {
print(error)
}
Or you can use the following model to always map the id as Int, even if your server sends it as String:
struct ResponseDataModel: Codable{
let data : DataClass?
enum CodingKeys: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}
struct DataClass: Codable {
let id: Int?
let name: String?
let age: Int?
enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try values.decodeIfPresent(Int.self, forKey:.id)
} catch DecodingError.typeMismatch {
if let idString = try values.decodeIfPresent(String.self, forKey:.id) {
id = Int(idString)
} else {
id = nil
}
}
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}

First of all, here are some key points to take case when using Codable for parsing.
There is no need to every time implement enum CodingKeys if the property names and keys have exactly same name.
Also, no need to implement init(from:) if there no specific parsing requirements. Codable will handle all the parsing automatically if the models are written correctly as per the format.
So, with the above 2 improvements your ResponseDataModel looks like,
struct ResponseDataModel : Codable{
let data: DataClass?
}
Now, for DataClass you simply need to add an if-else condition to handle the Int and String cases. Implementing generics is not needed here.
Use String or Int as the type for id. And add the conditions accordingly. In the below code, I'm using id as String.
struct DataClass : Codable {
let id : String //here....
let name : String?
let age : Int?
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
age = try values.decodeIfPresent(Int.self, forKey: .age)
if let id = try? values.decode(Int.self, forKey: .id) {
self.id = String(id)
} else {
self.id = try values.decode(String.self, forKey:.id)
}
}
}

As per #Joakim Danielson example provided, you can reach desired result by attempting to decode value for each type.
struct Response: Decodable {
let id: String
let name: String?
let age: Int?
private enum CodingKeys: String, CodingKey {
case data
}
private enum NestedCodingKeys: String, CodingKey {
case id
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nestedContainer = try container.nestedContainer(
keyedBy: NestedCodingKeys.self,
forKey: .data
)
if let id = try? nestedContainer.decode(Int.self, forKey: .id) {
self.id = String(id)
} else {
id = try nestedContainer.decode(String.self, forKey: .id)
}
name = try nestedContainer.decodeIfPresent(String.self, forKey: .name)
age = try nestedContainer.decodeIfPresent(Int.self, forKey: .age)
}
}
As #gcharita illustrated you can also catch DecodingError, but do-catch statement for decode(_:forKey:) would act only as an early exit, since it throws one of the following errors - typeMismatch, keyNotFound or valueNotFound for that particular key-value pair.

Related

Sqlite.swift: how I can use column aliasing(AS statment) after join for decoding?

I want to decode the result of query to my model that I used it in my remote request but after join i cant decode with codingKeys because sqlite.swift replaces the double-cotation and it can't find the column name
my code to get data from sqlite table:
let questions = Table("questions")
let replies = Table("replies")
let id = Expression<Int64>("id")
let questionId = Expression<Int64>("question_id")
let query: QueryType = questions.join(replies, on: questions[id] == replies[questionId])
let rows = try! db.prepare(query)
let values: [MyModel] = try rows.map({ try $0.decode() })
MyModel:
struct MyModel: Decodable, Identifiable {
var id: Int
var questionText: String
var replyText: String
enum CodingKeys: String, CodingKey {
case id = "questions.id"
case questionText = "questions.text"
case replyText = "replies.text"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try! container.decode(Int.self, forKey: .id)
self.questionText = try! container.decode(String.self, forKey: .questionText)
self.replyText = try! container.decode(String.self, forKey: .replyText)
}
}
But I faced with this error:
Fatal error: 'try!' expression unexpectedly raised an error: No such column `"questions.id"` in columns [ "\"questions\".\"id\"", "\"questions\".\"text\"", "\"replies\".\"text\"", "\"replies\".\"question_id\""]

Return from initializer without initializing all stored properties Swift Xcode 10.0

All the variable in my structure are optional then also in constructor I am getting this issue?
"Return from initializer without initializing all stored properties"
struct Conversation : Codable {
let chat_id : String?
let id : String?
let name : String?
let profile_pic : String?
let last_message_from : String?
let message : String?
let time : String?
let unread_count : String?
let member_count : String?
var type : ChatType = .Single
var doctors:[Doctors]?
enum CodingKeys: String, CodingKey {
case chat_id = "chat_id"
case id = "id"
case name = "name"
case profile_pic = "profile_pic"
case last_message_from = "last_message_from"
case message = "message"
case time = "time"
case unread_count = "unread_count"
case member_count = "member_count"
case doctors = "doctors"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
chat_id = try values.decodeIfPresent(String.self, forKey: .chat_id)
id = try values.decodeIfPresent(String.self, forKey: .id)
name = try values.decodeIfPresent(String.self, forKey: .name)
profile_pic = try values.decodeIfPresent(String.self, forKey: .profile_pic)
last_message_from = try values.decodeIfPresent(String.self, forKey: .last_message_from)
message = try values.decodeIfPresent(String.self, forKey: .message)
time = try values.decodeIfPresent(String.self, forKey: .time)
unread_count = try values.decodeIfPresent(String.self, forKey: .unread_count)
member_count = try values.decodeIfPresent(String.self, forKey: .member_count)
doctors = try values.decodeIfPresent([Doctors].self, forKey: .doctors)
}
init(doctor:Doctors) {
self.id = doctor.doctorId
self.profile_pic = doctor.doctorPic
self.type = .Single
}
}
If you create an initializer, you need to specify the value for all stored properties in the initializer, you cannot use the default values of your properties. So even if you declare your properties as Optional, you need to assign nil value to them in your initializer if you want them to be nil.
Unrelated to your issue, but there's no need to declare CodingKeys if all of your property names match your JSON keys and there's also no need to manually write the init(from:) initializer, the compiler can automatically synthesise that for you in your simple case. However, you should conform to the Swift naming convention, which is lowerCamelCase for variable names (including enum cases), so rename your properties accordingly and then you'll need CodingKeys.
Be aware that a lot of your types don't actually make sense. Why are the variables called count Strings? If they're coming as Strings from the backend, convert them to Ints in init(from:). Also, in your init(doctor:) it would make sense to actually add doctor to your doctors array.
struct Conversation : Codable {
let chatId: String?
let id: String?
let name: String?
let profilePic: String?
let lastMessageFrom: String?
let message: String?
let time: String?
let unreadCount: String?
let memberCount: String?
var type: ChatType = .single
var doctors:[Doctors]?
enum CodingKeys: String, CodingKey {
case chatId = "chat_id"
case id
case name
case profilePic = "profile_pic"
case lastMessageFrom = "last_message_from"
case message
case time
case unreadCount = "unread_count"
case memberCount = "member_count"
case doctors
}
init(doctor:Doctors) {
self.id = doctor.doctorId
self.profilePic = doctor.doctorPic
self.type = .single
self.chatId = nil
self.name = nil
self.lastMessageFrom = nil
self.message = nil
self.time = nil
self.unreadCount = nil
self.memberCount = nil
self.doctors = [doctor]
}
}

Swift: decode data into object which is an inherited class comes error

I'm working on decode data into object. The model classes are:
class ZZBaseObj: Codable {
var fid: String
init() {
fid = ""
}
private enum CodingKeys:String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
fid = try values.decode(String.self, forKey: CodingKeys.id)
}
}
class ZZFileObj: ZZBaseObj {
var columns:[String]
override init() {
columns = [String]()
super.init()
}
private enum CodingKeys:String, CodingKey {
case columns
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
columns = try values.decode([String].self, forKey: CodingKeys.columns)
try super.init(from: decoder)
}
}
The json data looks like:
(
{
columns = (
"first name",
last,
subject,
address,
age
);
id = "1lYc8iIzMdfdfgjGYMKwO4-X3fWe-4GlzfQ-dfs";
},
{
columns = (
"2018/1/30",
100,
"9800"
);
id = "1fdsafdgfQL-fdfdf-fdf";
}
)
When I try to use JSONDecoder() to decode the data into object, there is no error. I get the resultArray, and in the console, I can see the elements. Then it comes to EXC_BAD_ACCESS when comes to following code:
let obj = resultArray[0]
let content = obj.columns //---------error happens here
I understand it is a memory issue, but when I try to set breakpoint here, and in lldb input:
po obj.columns
It prints out the content of the columns.
This confuses me. As the class ZZFileObj is inherited from ZZBaseObj, and I guess there is something missing for inheritance. I try to make a new class which includes fid, and columns, and that comes no error. However, there a common attribute so I need such a base class for the models.
Please help take a look.
This is an issue in Swift 4.1.
https://bugs.swift.org/browse/SR-7090
You can fix for now by removing Codable conformance from base class. And once the issue is fixed in coming version of Xcode/Swift, you can revert back.
Here is how you can achieve this with minimal effort to revert back in future by moving Codable to base class. You can also try in beta version of Xcode 10 where your current code might work as expected.
class ZZBaseObj {
var id: String
init() {
id = ""
}
private enum CodingKeys:String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: CodingKeys.id)
}
}
class ZZFileObj: ZZBaseObj, Codable {
var columns:[String]
override init() {
columns = [String]()
super.init()
}
private enum CodingKeys:String, CodingKey {
case columns
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
columns = try values.decode([String].self, forKey: CodingKeys.columns)
try super.init(from: decoder)
}
}
let json = """
[
{
"columns":["some text 1", "some text 2"],
"id": "1lYc8iIzMdfdfgjGYMKwO4-X3fWe-4GlzfQ-dfs"
},
{
"columns":["some text 3", "some text 4"],
"id": "1lYc8iIzMdfdfgjGYMKwO4-X3fWe-4GlzfQ-dfs"
}
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
let obj = try! decoder.decode([ZZFileObj].self, from: json)
print(obj.first!.columns)
print(obj.first!.id)
Output:
["some text 1", "some text 2"]
1lYc8iIzMdfdfgjGYMKwO4-X3fWe-4GlzfQ-dfs

Swift 4 JSONDecoder optional variable

I have a Codable struct myObj:
public struct VIO: Codable {
let id:Int?;
...
var par1:Bool = false; //default to avoid error in parsing
var par2:Bool = false;
}
When I do receive JSON, I don't have par1 and par2 since these variables are optional. During parsing I get an error:keyNotFound(CodingKeys(stringValue: \"par1\", intValue: nil)
How to solve this?
If you have local variables you have to specify the CodingKeys
public struct VIO: Codable {
private enum CodingKeys : String, CodingKey { case id }
let id:Int?
...
var par1:Bool = false
var par2:Bool = false
}
Edit:
If par1 and par2 should be also decoded optionally you have to write a custom initializer
private enum CodingKeys : String, CodingKey { case id, par1, par2 }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
par1 = try container.decodeIfPresent(Bool.self, forKey: .par1)
par2 = try container.decodeIfPresent(Bool.self, forKey: .par2)
}
This is Swift: No trailing semicolons

Convert nil to empty string using JSONDecoder and Swift

I am using Swift 4 and JSONDecoder. I have the following structure:
struct Customer: Codable {
var id: Int!
var cnum: String!
var cname: String!
}
Note: the fields cannot be made optional.
Now I have a JSON string:
[
{
"id": 1,
"cnum": "200",
"cname": "Bob Smith"
},
{
"id": 2,
"cnum": "201",
"cname": null
}
]
And to decode it, I use the following:
let decoder = JSONDecoder()
let customers = try decoder.decode([Customer].self, from: json)
Everything works fine except the null data gets converted to nil. My question is, what would be the easiest way to convert incoming nil to an empty string ("")?
I would like to do this with the minimum amount of code but I'm not sure about the correct approach and at what point can the nil be converted to an empty string. Thank you beforehand.
You can use backing ivars:
struct Customer: Codable {
var id: Int
var cnum: String {
get { _cnum ?? "" }
set { _cnum = newValue }
}
var cname: String {
get { _cname ?? "" }
set { _cname = newValue }
}
private var _cnum: String?
private var _cname: String?
private enum CodingKeys: String, CodingKey {
case id, _cnum = "cnum", _cname = "cname"
}
}
Due to the custom CodingKeys, the JSON decoder will actually decode to _cnum and _cname, which are optional strings. We convert nil to empty string in the property getters.
You can use decodeIfPresent method.
struct Source : Codable {
let id : String?
enum CodingKeys: String, CodingKey {
case id = "id"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id) ?? "Default value pass"
}
}
You should make a computed variable that will have the original value if it's not nil and an empty string if it is.
var cnameNotNil: String {
return cname ?? ""
}
The usual way is to write an initializer which handles the custom behavior. The null value is caught in an extra do - catch block.
struct Customer: Codable {
var id: Int
var cnum: String
var cname: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
cnum = try container.decode(String.self, forKey: .cnum)
do { cname = try container.decode(String.self, forKey: .cname) }
catch { cname = "" }
}
}
Use decodeIfPresent if the value from response might be null
cnum = try container.decode(String.self, forKey: .cnum)
cname = try container.decodeIfPresent(String.self, forKey: .cname)