Failable Initializers with Codable

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:

  1. Abandon optionality for the company property, and define the enum as:

    enum Company: Int {
    case unspecified = 0
    case toyota
    case ford
    case gm
    }

    ...closely matching the JSON, or,

  2. 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 returning 0 (I believe JSON does have a "null" value, but I'm not sure how JSONDecoder 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 classes, so the example initializer will work for as-is for structs and enums, 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:

  1. String data(using:) with an encoding of .utf8 can't fail so the guard can be replaced with a forced-unwrapped.
  2. 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



Leave a reply



Submit