I'm trying to adopt the Codable
protocol for an object that must be instantiated from a JSON my web service returns in response to one of the API calls.
One of the properties is of enumeration type, and optional: nil
means that none of the options defined by the enum
has been chosen.
The enum
constants are Int
-based and start at 1
, not 0
:
class MyClass: Codable {
enum Company: Int {
case toyota = 1
case ford
case gm
}
var company: Company?
This is because the value 0
on the corresponding JSON entry is reserved for "not set"; i.e. it should be mapped to nil
when setting initializing the company
property from it.
Swift's enum initializer init?(rawValue:)
provides this functionality out-of-the-box: an Int
argument that does not match the raw value of any case will cause the initializer to fail and return nil. Also, Int
(and String) -based enums can be made to conform to Codable
just by declaring it in the type definition:
enum Company: Int, Codable {
case toyota = 1
case ford
case gm
}
The problem is, my custom class has more than 20 properties, so I really really want to avoid having to implement init(from:)
and encode(to:)
, relying instead on the automatic behavior obtained by providing the CondingKeys
custom enumeration type.
This results in initialization of the whole class instance failing because it seems the "synthesized" initializer cannot infer that an unsupported raw value of the enum type should be treated as nil
(even though the target property is clearly marked as optional, i.e. Company?
).
I think this is so because the initializer provided by Decodable
can throw, but it can not return nil:
// This is what we have:
init(from decoder: Decoder) throws
// This is what I would want:
init?(from decoder: Decoder)
As a workaround, I have implemented my class as follows: map the JSON's integer property into a private, stored Int
property of my class that serves as storage only, and introduce a strongly-typed computed property that acts as a bridge between the storage and the rest of my app:
class MyClass {
// (enum definition skipped, see above)
private var companyRawValue: Int = 0
public var company: Company? {
set {
self.companyRawValue = newValue?.rawValue ?? 0
// (sets to 0 if passed nil)
}
get {
return Company(rawValue: companyRawValue)
// (returns nil if raw value is 0)
}
}
enum CodingKeys: String, CodingKey {
case companyRawValue = "company"
}
// etc...
My question is: Is there a better (simpler/more elegant) way, that:
- Does not require duplicated properties like my workaround, and
- Does not require fully implementing
init(from:)
and/orencode(with:)
, perhaps implementing simplified versions of these that delegate to the default behavior for the most part (i.e. do not require the whole boilerplate of manually initializing/encoding each and every property)?
Addendum: There's a third, also inelegant solution that didn't occur to me when I first posted the question. It involves using an artificial base class just for the sake of automatic decoding. I will not use it, but just describe it here for the sake of completeness:
// Groups all straight-forward decodable properties
//
class BaseClass: Codable {
/*
(Properties go here)
*/
enum CodingKeys: String, CodingKey {
/*
(Coding keys for above properties go here)
*/
}
// (init(from decoder: Decoder) and encode(to:) are
// automatically provided by Swift)
}
// Actually used by the app
//
class MyClass: BaseClass {
enum CodingKeys: String, CodingKey {
case company
}
var company: Company? = nil
override init(from decoder: Decoder) throws {
super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
if let company = try? values.decode(Company.self, forKey: .company) {
self.company = company
}
}
}
...But this is a really ugly hack. Class inheritance hierarchy shouldn't be dictated by this type of shortcomings.
nil
toself
inside a non-failable, throwing initializer... That answer assigns a default actual value... – Ginkgonil
assigned to my optional property, butDecodable
works by throwing... – Ginkgoclass MyClass: Codable { enum Company: Int, Codable { case toyota = 1, ford, gm } var company: Company? required init(from decoder: Decoder) throws { company = try Company(rawValue: decoder.singleValueContainer().decode(Int.self)) } }
– Volkinit(from decoder: Decoder)
); my class has a lot of properties besidescompany
... – Ginkgotry? ....
? You can writeif let propertyName = try? .... { do what you need }
. You dont actually needinit?(from decoder: Decoder)
. – Tori