Search code examples
initializationswift4initializercodable

Can you “extend” (i.e. add additional initialization logic to) the auto-generated constructor for a Codable object?


When you implement Codable on an object, the compiler can automatically generate a constructor for you. However it only does this if you haven't written an initializer that accepts a decoder yourself.

That said, we have an object with ~50 let properties which are set from the decoder, but we also have five computed properties which are based on those let properties.

Technically if we could compute them in the initializer, after the decoder has set the other 50, we could simply store the results in let vars of their own, eliminating the need for computed properties altogether.

The problem is, as mentioned, if you implement your own initializer, the compiler doesn't auto-generate one for you so you're left not just initializing your 'computed' values, but all values.

So is there a way you can insert yourself as part of the initialization/decoding process without having to fully rewrite the initializer yourself?


Solution

  • 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)