I have following general structure where data can be anyother codable object
struct GeneralResponse<T:Codable>: Codable {
let message: String
let status: Bool
let data: T?
enum CodingKeys: String, CodingKey {
case message = "Message"
case status = "Status"
case data = "Data"
}
}
I have Following Like response codable class which will be used as data in GeneralResponse
class ImgLike: Codable {
let id: Int?
let imageID, user: String?
#available(*, deprecated, message: "Do not use.")
private init() {
fatalError("Swift 4.1")
}
enum CodingKeys: String, CodingKey {
case id = "ID"
case imageID = "ImageID"
case user = "User"
}
}
Question 1 : When the token expires on API, The response data is empty {} still It show ImgLike object with all nil properties. Why it not show data to be nil ?
Then If I check object?.data == nil it is showing false !! So I need to check each property
Question 2 : In ImgLike If I am using custom encode function. GeneralResponse not parsed with ImgLike not parsed it shows error in catch statement
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
imageID = try values.decode(String.self, forKey: .imageID)
user = try values.decode(String.self, forKey: .user)
do {
id = Int(try values.decode(String.self, forKey: .id))
} catch {
id = try values.decode(Int.self, forKey: .id)
}
}
The equivalents of Swift-nil are JSON-null and JSON-not-set. {} is a valid dictionary in JSON and so not Swift-nil.
I guess that you mean that you get an error incase you use the custom decoder function? That‘s expected since the default decoder uses decodeIfPresent instead of decode to decode optionals since they are allowed not to be set.
And since you decode an empty dictionary {} none of the values are present/set.
Counting keys in dict to avoid decoding from JSON-{}
This CodingKey-struct accepts every key it gets.
fileprivate struct AllKeysAllowed: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
}
struct GeneralResponse<T:Codable>: Decodable {
let message: String
let status: Bool
let data: T?
enum CodingKeys: String, CodingKey {
case message = "Message"
case status = "Status"
case data = "Data"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
message = try container.decode(String.self, forKey: .message)
status = try container.decode(Bool.self, forKey: .status)
Decode .data to a container which had all keys accepted.
Then the number of keys in the JSON-dictionary is readable with dataContainer.allKeys.count.
let dataContainer = try container.nestedContainer(keyedBy: AllKeysAllowed.self, forKey: .data)
if dataContainer.allKeys.count != 0 {
data = try container.decode(T.self, forKey: .data)
} else {
data = nil
}
}
}
Note that the default Codable implementation uses decodeIfPresent instead of decode. decodeIfPresent will not throw an error even if the key is not present in the JSON. It will simply return nil. So an empty JSON dictionary has no KVPs, so all the properties are set to nil.
In your custom implementation of Codable, you are using decode, which will throw an error if the key is not found.
The reason why object?.data != nil is because object?.data is a ImgLike???. You are wrapping an optional in an optional in an optional. I see that the type of object is GeneralResponse<ImgLike?>?. This will make data's type be ImgLike??. I don't think this is your intention. You probably intended to use GeneralRepsonse<ImgLike>. You might have forgotten to unwrap an optional somewhere. You also need to unwrap the outermost optional:
if let nonNilObject = object {
// nonNilObject.data is of type ImgLike?
}
As already mentioned the decoder does not treat an empty dictionary as nil.
You can add this functionality in a generic way with a tiny protocol and an extension of KeyedDecodingContainer
public protocol EmptyDictionaryRepresentable {
associatedtype CodingKeys : RawRepresentable where CodingKeys.RawValue == String
associatedtype CodingKeyType: CodingKey = Self.CodingKeys
}
extension KeyedDecodingContainer {
public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T?
where T : Decodable & EmptyDictionaryRepresentable
{
guard contains(key) else { return nil }
let container = try nestedContainer(keyedBy: type.CodingKeyType.self, forKey: key)
return container.allKeys.isEmpty ? nil : try decode(T.self, forKey: key)
}
}
Just add EmptyDictionaryRepresentable conformance to ImgLike, the associated types are inferred.
class ImgLike: Codable, EmptyDictionaryRepresentable {
The properties in ImgLike could be even declared as non-optional
Related
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.
I have a service whose response vary based on the orderNumer(input parameter which I pass to service). all most all object are same but only one object(ex meta object which is dictionary)vary based on order number. But I would like to reuse the same model class everywhere but due to different meta object can't be able to create that meta object in model class.I can achieve it by creating individual model class but not a right solution.
struct BookingInfo: Codable {
let created_time: Int
// Some other 20 key value pairs
let oms_meta: Meta /* oms_meta": {
"package": {
"description": "gzhdjjdjd"
}*/
}
// Meta for Order1
struct Meta: Codable {
let package: Description
enum CodingKeys : String, CodingKey {
case package = "package"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
package = try values.decode(Description.self, forKey: .package)
}
}
// Meta for Order 2
struct Meta: Codable {
let customer_payment_details: Int
let items: Int // this can be anything dictinary or some time array of dictionary
enum CodingKeys : String, CodingKey {
case package = "customer_payment_details"
case items = "items"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
customer_payment_details = try values.decode(Int.self, forKey: .package)
items = try values.decode(Int.self, forKey: .package)
}
}
Only meta parameters are varying from service to service.
Thanks in advance
If you want to only use 1 model, you should provide a variable for all the possible keys in the dictionary and set them as optional.
Let's say all orders have an orderNumber, but only some have an orderImage you could do it like this:
struct Order: Codable {
var orderNumber: Int
var orderImage: Data?
var orderName: String?
var orderDescription: String?
}
This way, the returned data only has to contain an orderNumber to be able to decode it.
Edit: You'd have to make a seperate model for each type of 'meta data' as they seem to be very different from eachother. Note that there is nothing wrong with creating a lot of model classes as long as they represent a specific data object.
[
{
"created_time": 1,
"oms_meta": {
"name": "1"
}
},
{
"created_time": 1,
"oms_meta": [
1
]
}
]
Suppose we have a response something like this, where you can have multiple oms_meta objects. So what we can do here is to specify an enum which will contain both array as well as dictionary, it doesn't matter if the service sends you array or dictionary if you need to specify other data types mentioned it in that enum. So it will be like,
struct BookingInfoElement: Codable {
let createdTime: Int?
let omsMeta: OmsMetaUnion?
enum CodingKeys: String, CodingKey {
case createdTime = "created_time"
case omsMeta = "oms_meta"
}
}
enum OmsMetaUnion: Codable {
case integerArray([Int])
case omsMetaClass(OmsMetaClass)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([Int].self) {
self = .integerArray(x)
return
}
if let x = try? container.decode(OmsMetaClass.self) {
self = .omsMetaClass(x)
return
}
throw DecodingError.typeMismatch(OmsMetaUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for OmsMetaUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .integerArray(let x):
try container.encode(x)
case .omsMetaClass(let x):
try container.encode(x)
}
}
}
struct OmsMetaClass: Codable {
let name: String?
enum CodingKeys: String, CodingKey {
case name = "name"
}
}
typealias BookingInfo = [BookingInfoElement]
So, if you specifically want to have multiple types for a single variable you can create another enum with that multiple types and assign accordingly. You can access that variable using switch like, and check on array and dictionary types.
After doing some analyses I found the solution for my requirement. As I wanted to reuse the Model class and OMS_Meta dictionary different from product to product, I'm adding omsMeta to JSONDecoder().userInfo property
struct BookingInfo: Codable {
let created_time: Int
let oms_meta: [String: Any]
}
public static let metaUserInfo = CodingUserInfoKey(rawValue: "oms_meta")!
decoder.userInfo[BookingInfo.metaUserInfo] = jsonDecode
By doing this I can re use my model class.
I have JSON that looks like this:
{
success: true,
message: null,
messages: null,
data: [
[ ... ]
]
}
Now I am wondering, I can solve this with:
struct Something: Codable {
let data: [[Data]]
}
something.data.flatMap { $0 }
But I would rather do:
struct Something: Codable {
let data: [Data]
}
I already know I can achieve navigation through JSON with sets of CodingKeys enums and container.nestedContainer(...) but how do I achieve this when there's no key, but just an array in an array? Can I achieve this with the custom init on Decodable and if so, how?
A possible solution is to write a custom initializer for flattening the array
struct Root : Decodable {
let success : Bool
let data : [Foo]
private enum CodingKeys : String, CodingKey { case success, data }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
success = try container.decode(Bool.self, forKey: .success)
let arrayData = try container.decode([[Foo]].self, forKey: .data)
guard !arrayData.isEmpty else { throw DecodingError.dataCorruptedError(forKey: .data, in: container, debugDescription: "Object is empty") }
data = arrayData.first!
}
}
struct Foo : Decodable { ... }
The type Data exists in the Foundation framework. You are strongly discouraged from using it as custom type.
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
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)