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:
init(from:)
and/or encode(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.
I think I had a similar issue to yours, if I'm understanding correctly. In my case, I wrote a wrapper for each enum in question:
struct NilOnFail<T>: Decodable where T: Decodable {
let value: T?
init(from decoder: Decoder) throws {
self.value = try? T(from: decoder) // Fail silently
}
// TODO: implement Encodable
}
Then use it like this:
class MyClass: Codable {
enum Company: Int {
case toyota = 1
case ford
case gm
}
var company: NilOnFail<Company>
...
The caveat being, of course, that wherever you need to access the value of company
you need to use myClassInstance.company.value
From swift 5 you can use Property Wrappers. https://docs.swift.org/swift-book/LanguageGuide/Properties.html
In your case main struct will be like:
@propertyWrapper
public struct NilOnFailCodable<ValueType>: Codable where ValueType: Codable {
public var wrappedValue: ValueType?
public init(wrappedValue: ValueType?) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
self.wrappedValue = try? ValueType(from: decoder)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let value = wrappedValue {
try container.encode(value)
} else {
try container.encodeNil()
}
}
}
Usage
struct Model: Codable {
@NilOnFailCodable var val: Enum?
enum Enum: Int, Codable {
case holdUp = 0
case holdDown = 1
}
}
And example
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let s = #"{"val": 2}"#
let data = s.data(using: .utf8)
let dec = decoder.decode(Model.self, from: data!)
print(dec)
let enc = encoder.encode(dec)
print(decoder.decode(Model.self, from: enc))
Will print
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: nil))
nil
And for value "val": 1
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
Model(_val: NilOnFailCodable<Model.Enum>(wrappedValue: Optional(Model.Enum.holdDown)))
Optional(1)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With