Property wrappers not being respected in custom Decodable initialisers

I use property wrappers to specify the date format I want to use for the Decodable protocol. However, when I try to use them in a custom init(from decoder: Decoder), it starts giving me an error.
The question is: How can I make it respect the property wrappers?
I'm expect decode(Date.self,... is where the issue is, but I haven't found a way to insert a property wrapper there.

As an example, here are 2 almost identical structs. The Ok struct decodes perfectly, while NotOk throws an error, which is what I'm looking to fix.

struct Ok: Decodable {
    @LADateWithoutT var date: Date

    enum CodingKeys: String, CodingKey { case date }
}

struct NotOk: Decodable {
    @LADateWithoutT var date: Date

    enum CodingKeys: String, CodingKey { case date }

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        date = try values.decode(Date.self, forKey: .date) // <- ISSUE HERE
    }
}

let json = #"{"date":"2021-05-10 12:14:28Z"}"#.data(using: .utf8)!
let decoder = JSONDecoder()
do {
    let _ = try decoder.decode(Ok.self, from: json) // <- WORKS FINE
} catch { print("OK: \(error)") }
    
do {
    let _ = try decoder.decode(NotOk.self, from: json) // <- THROWS AN ERROR
} catch { print("NotOK: \(error)") }
Property wrapper

import Foundation

/// Wrapper for dates formatted as `"YYYY-MM-DD HH:mm:ssZ"`.
@propertyWrapper
public struct LADateWithoutT: Codable {
    static internal var formatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY-MM-DD HH:mm:ssZ"
        return formatter
    }
    
    public var wrappedValue: Date
    
    public init(wrappedValue: Date) {
        self.wrappedValue = wrappedValue
    }
    
    public init(from decoder: Decoder) throws {
        let strValue = try decoder.singleValueContainer().decode(String.self)
        guard let date = LADateWithoutT.formatter.date(from: strValue) else {
            throw LAClientError.dateIsNil
        }
        self.wrappedValue = date
    }
    
    public func encode(to encoder: Encoder) throws {
        let strValue = LADateWithoutT.formatter.string(from: wrappedValue)
        try strValue.encode(to: encoder)
    }
}

The difference between the two is that the automatically synthesized decoding is actually decoding LADateWithoutT, not Date. If you changed from Date.self to LADateWithoutT.self (and assign the decoded value to _date, the underlying property wrapper instance), you would get the same behavior as the automatic one.

1 Like

Thanks a lot Mike, that worked like a charm!

1 Like