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)
Related
I've read this and this and spent many hours perusing SO, but I can't seem to figure out how to properly use Decode with an empty array.
Original Code
At first, I had no problems with my code.
Here was my original struct:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
enum Category: String, Codable {
case dailyJobMorning
case dailyJobEvening
case weeklyJob1
case weeklyJob2
case jobBonus
case quickPoints
case jobJar
case dailyHabit
case weeklyHabit
case habitBonus
}
}
And here was my function:
static func observeJobs(completion: #escaping () -> Void) {
FB.ref
.child(FB.jobs)
.observe(.value, with: { (snapshot) in
guard snapshot.exists() else {
completion()
return
}
for item in snapshot.children {
guard let snap = item as? DataSnapshot else { return }
guard let value = snap.value as? [String: Any] else { return }
do {
let jsonData = try JSONSerialization.data(withJSONObject: value, options: [])
let habit = try JSONDecoder().decode(JobHabit.self, from: jsonData)
jobsMasterList.append(habit)
} catch let error {
print(error)
manuallyDecodeJobHabitAndAddToArray(.dailyJobMorning, value)
}
}
completion()
})
}
That all worked great. No problems.
But then...
I added in another parameter to the struct. I added in a points parameter that could potentially be empty. It's not optional, but it could be empty. (It's the last parameter.)
Like this:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
var points: [Point] // <=== new parameter that messed everything up
}
And that caused the decode function to fail with this error:
keyNotFound(CodingKeys(stringValue: "points", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "points", intValue: nil) ("points").", underlyingError: nil))
The error makes sense. The decoder can't find a value. Well, if it's empty, then Firebase will return nothing. So that's expected behavior. But why can't the decoder account for that?
I read up on decoding optional values and came up with an initializer for the struct, like so:
struct JobHabit: Codable {
var name: String
var description: String
var assigned: String
var order: Int
var category: Category
var active: Bool
var altName: String
var altNameDate: Double
var altAssigned: String
var altCategory: Category
var altOrder: Int
var points: [Point]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.description = try container.decode(String.self, forKey: .description)
self.assigned = try container.decode(String.self, forKey: .assigned)
self.order = try container.decode(Int.self, forKey: .order)
self.category = try container.decode(Category.self, forKey: .category)
self.active = try container.decode(Bool.self, forKey: .active)
self.altName = try container.decode(String.self, forKey: .altName)
self.altNameDate = try container.decode(Double.self, forKey: .altNameDate)
self.altAssigned = try container.decode(String.self, forKey: .altAssigned)
self.altCategory = try container.decode(Category.self, forKey: .altCategory)
self.altOrder = try container.decode(Int.self, forKey: .altOrder)
self.points = try container.decodeIfPresent([Point].self, forKey: .points) ?? []
}
}
That created a new issue. The issue is that I can't create new instances of my struct anywhere else in the app. When I try doing this:
static let job120 = JobHabit(name: "💳 Bills & finances",
description: "record receipts\nupdate accounts\npay bills\nfiling (max 1 hour)",
assigned: "none",
order: 19,
category: .weeklyJob1,
active: false,
altName: "💳 Bills & finances",
altNameDate: 0,
altAssigned: "none",
altCategory: .weeklyJob1,
altOrder: 19,
points: [])
I get the following error message:
Extra argument 'name' in call
Apparently, the initializer is making it so I can't create new instances of the struct? I'm really struggling to figure out how to decode a potentially empty array from Firebase.
What am I doing wrong? Everything worked fine with decoding BEFORE I added in that empty points array. Once I added in the points parameter, decode couldn't handle it, and so I had to add in the initializers in the struct. Now my other code doesn't work.
Oh, and here is my JSON tree sample:
{
"-MOESVPtiXtXQh19sWBP" : {
"active" : true,
"altAssigned" : "Dad",
"altCategory" : "dailyJobMorning",
"altName" : "🔍 Morning Job Inspections",
"altNameDate" : 0,
"altOrder" : 0,
"assigned" : "Dad",
"category" : "dailyJobMorning",
"description" : "set job timer\nvisually inspect each person's job...",
"name" : "🔍 Morning Job Inspections",
"order" : 0
}
}
When you have an API, which can return a struct with either filled or empty value, you must declare this variable as an optional. In other case how are you going to handle it in your code later?
As apple documentation says, an optional is:
A type that represents either a wrapped value or nil, the absence of a value.
Doesn't this make sense?
So to fix existing issues just declare your points variable as [Point]?. If there is no value for points presented in your API response, it will remain nil. Later in your code use standard optionals' unwrappers to check whether there is value presented or not.
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.
How can I decode the following JSON to a Codable object in Swift?
{"USD":"12.555", "EUR":"11.555"}
Here's the struct i'm using:
struct Prices: Codable {
var USD: Double
var EUR: Double
private enum CodingKeys: String, CodingKey {
case USD = "USD"
case EUR = "EUR"
}
init() {
self.USD = 0.0
self.EUR = 0.0
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = try container.decode(Double.self, forKey: .USD)
self.EUR = try container.decode(Double.self, forKey: .EUR)
}
}
The error I'm getting is
Error typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "USD", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
I think your struct is incorrect and will be hard to maintain if you want to download more currency rates so I suggest a different approach. First create a struct for holding a currency rate
struct CurrencyRate {
let currency: String
let rate: Decimal?
}
Then decode the json as a dictionary and use map to convert it into an array of CurrencyRate
var rates = [CurrencyRate]()
do {
let result = try JSONDecoder().decode([String: String].self, from: json)
rates = result.map { CurrencyRate(currency: $0.key, rate: Decimal(string: $0.value))}
} catch {
print(error)
}
Two notes about CurrencyRate
You have two currencies in a rate so normally you also have another property named baseCurrency or otherCurrency or something similar but if that other currency always is the same you can omit it.
Depending on your use case it might also be a good idea to make a new type for currency properties, Currency
The values you're getting are Strings, so your struct should simply be
struct Prices: Codable {
let USD: String
let EUR: String
}
Depending you how you inted to use those price values, converting them to double may be inadvisable, because floating-point math is weird and Decimals may be better suited.
You have to decode the values as String and then cast it to Double. So, either Cast String to Double and provide a default value, like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = Double(try container.decode(String.self, forKey: .USD)) ?? 0
self.EUR = Double(try container.decode(String.self, forKey: .EUR)) ?? 0
}
Or make the properties optional and remove the default values:
struct Prices: Codable {
var USD, EUR: Double?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = Double(try container.decode(String.self, forKey: .USD))
self.EUR = Double(try container.decode(String.self, forKey: .EUR))
}
Note: Also, you don't need CodingKeys as the property names are the same as keys in the data.
Alternatively, you can use the computed property method, like this:
struct Prices: Codable {
var USDString: String
var EURString: String
private enum CodingKeys: String, CodingKey {
case USDString = "USD"
case EURString = "EUR"
}
var USD: Double? { Double(USDString) }
var EUR: Double? { Double(EURString) }
}
Attempting to refactor some legacy JSON parsing code to use Codable and attempting to reuse the existing Swift structs, for simplicity pls consider the following JSON:
{
"dateOfBirth":"2016-05-19"
...
"discountOffer":[
{
"discountName":"Discount1"
...
},
{
"discountName":"Discount2"
...
}
]
}
In the legacy code, the Swift struct Discount has a property 'discountType' whose value is computed based on Member struct's 'dateOfBirth' which is obtained from the JSON, question is, how do I pass the Member's dateOfBirth down to the each Discount struct? Or is there a way for structs lower in the hierarchy to access structs higherup in the hierarchy?
struct Member: Codable {
var dateOfBirth: Date?
var discounts: [Discount]?
}
struct Discount: Codable {
var discountName: String?
var memberDateOfBirth: Date? // *** Need to get it from Member but how?
var discountType: String? // *** Will be determined by Member's dateOfBirth
public init(from decoder: Decoder) throws {
// self.memberDateOfBirth = // *** How to set from Member's dateOfBirth?????
// use self.memberDateOfBirth to determine discountType
...
}
}
I am not able to use the decoder's userInfo as its a get property. I thought of setting the dateOfBirth as a static variable somewhere but sounds like a kludge.
Would appreciate any help. Thanks.
You should handle this in Member, not Discount, because every Codable type must be able to be decoded independently.
First, add this to Discount so that only the name is decoded:
enum CodingKeys : CodingKey {
case discountName
}
Then implement custom decoding in Member:
enum CodingKeys: CodingKey {
case dateOfBirth, discounts
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dateOfBirth = try container.decode(Date.self, forKey: .dateOfBirth)
discounts = try container.decode([Discount].self, forKey: .discounts)
for i in 0..<discounts!.count {
discounts![i].memberDateOfBirth = dateOfBirth
}
}
The for loop at the end is where we give values to the discounts.
Going back to Discount, you can either make discountType a computed property that depends on memberDateOfBirth, or add a didSet observer to memberDateOfBirth, where you set discountType.
var discountType: String? {
if let dob = memberDateOfBirth {
if dob < Date(timeIntervalSince1970: 0) {
return "Type 1"
}
}
return "Type 2"
}
// or
var memberDateOfBirth: Date? {
didSet {
if let dob = memberDateOfBirth {
if dob < Date(timeIntervalSince1970: 0) {
discountType = "Type 1"
}
}
discountType = "Type 2"
}
}
You can access them like this
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.memberDateOfBirth = try values.decode(T.self, forKey: .result) //and whatever you want to do
serverErrors = try values.decode([ServerError]?.self, forKey: .serverErrors)
}
you can try in this way:
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(String.self, forKey: .memberDateOfBirth)
let formatter = "Your Date Formatter"
if let date = formatter.date(from: dateString) {
memberDateOfBirth = date
}
if you want to know more check this approach:
https://useyourloaf.com/blog/swift-codable-with-custom-dates/
For Dateformatter you can check :
Date Format in Swift
https://nsdateformatter.com
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