Swift Codable - How to Initialize an Optional Enum Property in a Failable Manner
After searching the documentation for the Decoder
and Decodable
protocols and the concrete JSONDecoder
class, I believe there is no way to achieve exactly what I was looking for. The closest is to just implement init(from decoder: Decoder)
and perform all the necessary checks and transformations manually.
Additional Thoughts
After giving some thought to the problem, I discovered a few issues with my current design: for starters, mapping a value of 0
in the JSON response to nil
doesn't seem right.
Even though the value 0
has a specific meaning of "unspecified" on the API side, by forcing the failable init?(rawValue:)
I am essentially conflating all invalid values together. If for some internal error or bug the server returns (say) -7
, my code won't be able to detect that and will silently map it to nil
, just as if it were the designated 0
.
Because of that, I think the right design would be to either:
Abandon optionality for the
company
property, and define theenum
as:enum Company: Int {
case unspecified = 0
case toyota
case ford
case gm
}...closely matching the JSON, or,
Keep optionality, but have the API return a JSON that lacks a value for the key "company" (so that the stored Swift property retains its initial value of
nil
) instead of returning0
(I believe JSON does have a "null" value, but I'm not sure howJSONDecoder
deals with it)
The first option requires to modify a lot of code around the whole app (changing occurrences of if let...
to comparisons against .unspecified
).
The second option requires modifying the server API, which is beyond my control (and would introduce a migration/ backward compatibility issue between server and client versions).
I think will stick with my workaround for now, and perhaps adopt option #1 some time in the future...
A failable initialiizer for Swift Decodable
so it looks like I needed to add a try? in the Recommended List init(from decoder:)
public struct RecommendedList: Decodable {
public let poster: Poster?
public let recommends: [Recommend]
enum CodingKeys: String, CodingKey {
case poster
case recommends
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
poster = try? container.decode(Poster.self, forKey: .poster)
recommends = try container.decode([Recommend].self, forKey: .recommends)
}
}
Convenience Failable Initializers assign self
Have you considered a factory method on NSURL
? It gets you most of the way there.
extension NSURL {
static func initialize(string: String, throwOnNil: Bool) throws -> NSURL? {
if let URL = NSURL(string: string) {
return URL
} else if throwOnNil {
throw MyURLError(string: string)
} else {
return nil
}
}
}
Use `self =` in convenience initializers to delegate to JSONDecoder or factory methods in Swift to avoid `Cannot assign to value: 'self' is immutable`
First, note that this limitation exists only for class
es, so the example initializer will work for as-is for struct
s and enum
s, but not all situations allow changing a class
to one of these types.
This limitation on class
initializers is a frequent pain-point that shows up often on this site (for example). There is a thread on the Swift forums discussing this issue, and work has started to add the necessary language features to make the example code above compile, but this is not complete as of Swift 5.4.
From the thread:
Swift's own standard library and Foundation overlay hack around this missing functionality by making classes conform to dummy protocols and using protocol extension initializers where necessary to implement this functionality.
Using this idea to fix the example code yields
final class Test: Codable {
let foo: Int
init(foo: Int) {
self.foo = foo
}
func jsonData() throws -> Data {
try JSONEncoder().encode(self)
}
}
protocol TestProtocol: Decodable {}
extension Test: TestProtocol {}
extension TestProtocol {
init(fromJSON data: Data) throws {
self = try JSONDecoder().decode(Self.self, from: data)
}
}
let test = Test(foo: 42)
let data = try test.jsonData()
let decodedTest = try Test(fromJSON: data)
print(decodedTest.foo)
which works fine. If Test
is the only type conforming to TestProtocol
, then only Test
will get this initializer.
An alternative is to simply extend Decodable
or another protocol to which your class conforms, but this may be undesirable if you do not want other types conforming to that protocol to also get your initializer.
Value of optional type Self? not unwrapped inside failable initializer with try? and Self, how to write without force unwrapping
try?
returns an optional. But in a failable initializer, you don't assign nil
to self
, you return nil
.
The following works:
extension Mappable {
init?(jsonString: String) {
guard let data = jsonString.data(using: .utf8) else {
return nil
}
do {
self = try JSONDecoder().decode(Self.self, from: data)
} catch {
return nil
}
}
}
As others have stated, a few other things to consider:
String data(using:)
with an encoding of.utf8
can't fail so theguard
can be replaced with a forced-unwrapped.- Consider changing this from a failable initializer to one that throws.
Can you “extend” (i.e. add additional initialization logic to) the auto-generated constructor for a Codable object?
What you are looking for is similar to the delegate pattern where the decoder informs its delegate that it has finished decoding. Sadly, it's not yet added to Swift. The closest I can think off is to use inheritance so Swift can auto generate the decoder for the those 50 let
in the base class, and you can initialize your computed properties in a subclass. For example:
class A: Decodable {
let firstName: String
let lastName: String
}
class B: A {
private var _fullName: String! = nil
var fullName: String { return _fullName }
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
_fullName = firstName + " " + lastName
}
}
Define your 50 properties in class A and leave all the computed properties in class B.
Or at your suggestion, you can also use lazy var
:
struct Model: Decodable {
let firstName: String
let lastName: String
// private(set) so users cannot change value of the
// pre-computed property
lazy private(set) var fullName = self.firstName + " " + self.lastName
}
// But you can't use a let here, since calling fullName
// for the first time will mutate the struct
var model = try JSONDecoder().decode(Model.self, from: json)
Related Topics
New Value Is Only Available in Sendasynchronousrequest - Swift
How Could I Request Text from a Website in Swift
Can the Byte Order of Double Be Safely Reversed
How Is a Global Variable Set to Private Understood in Swift
Does _Arraytype or _Arrayprotocol Not Available in Swift 3.1
Performing a Completion Handler Before App Launches
Nsmanagedobject Subclasses Duplicate Declaration
Label Showing Top of Screen Instead of Being on the Inputaccessoryview
Interrupted Purchase Not Calling Delegate After Accepting T&C
Getting the Time Remaining in the Time Interval of a Timer in Swift
How to Change the Order of Functions Triggered
Cannot Read Property 'Keycode' 'Character' Assertion Failure When Detect Modifier Key + Numeric Key
Is There Any Particular Use of Closure in Swift? and What's the Benefit
Error: Argument Type Double/String etc. Does Not Conform to Expected Type "Anyobject"
How to Decode Partially Double Serialized JSON String Using 'Codable' Protocol