Home > Enterprise >  Default value for Codable property that does not exists with PropertyWrapper
Default value for Codable property that does not exists with PropertyWrapper

Time:09-08

i created a propertyWrapper like this:

@propertyWrapper
public struct DefaultTodayDate: Codable {
    public var wrappedValue: Date

    private let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        formatter.dateFormat = "y-MM-dd'T'HH:mm:ss"

        return formatter
    }()

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        var stringDate = ""
        do {
            stringDate = try container.decode(String.self)
            self.wrappedValue = self.dateFormatter.date(from: stringDate) ?? Date()
        } catch {
            self.wrappedValue = Date()
        }
    }

    public func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

and a model like this:

struct MyModel: Codable {
    @DefaultTodayDate var date: Date
}

so, if i want to parse this json file, everything is ok:

let json = #"{ "date": "2022-10-10T09:09:09" }"#.data(using: .utf8)!
let result = try! JSONDecoder().decode(MyModel.self, from: json)

print(result) // result.date is: 2022-10-10 09:09:09  0000
-----

let json = #"{ "date": "" }"#.data(using: .utf8)!
let result = try! JSONDecoder().decode(MyModel.self, from: json)

print(result) // result.date is: Date()
-----

let json = #"{ "date": null }"#.data(using: .utf8)!
let result = try! JSONDecoder().decode(MyModel.self, from: json)

print(result) // result.date is: Date()

but i want to also parse a json without date property.but i get. fatal error:

let json = #"{ "book": "test" }"#.data(using: .utf8)!
let result = try! JSONDecoder().decode(MyModel.self, from: json)

// Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "date", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"date\", intValue: nil) (\"date\").", underlyingError: nil))

print(result) // i want to result.date be Date() 

CodePudding user response:

According to the proposal for property wrappers, properties annotated with a property wrapper gets translated to a computed property and a stored property. The stored property stores the property wrapper's instance, and the computed property gets (or sets) the wrapped value.

For example, this is one of the variations:

@Lazy var foo = 17

// ... implemented as
private var _foo: Lazy = Lazy(wrappedValue: 17)
var foo: Int { 
    get { _foo.wrappedValue }
    set { _foo.wrappedValue = newValue }
}

Notice that in all the variations, the stored property is always non-optional. This means that when generating the Codable conformance, the property wrapper will always be generated as a decode, rather than decodeIfPresent, and will throw an error if the key is not present.

So @DefaultTodayDate var date: Date is not possible, but we can still use DefaultTodayDate as a normal type, assuming you want to go down the route of using wrappers to parse this.

First, add a parameterless initialiser to DefaultTodayDate:

public init() {
    wrappedValue = Date()
}

Then do:

struct MyModel: Codable {
    enum CodingKeys: String, CodingKey {
        case dateWrapper = "date"
    }
    
    private var dateWrapper: DefaultTodayDate?
    
    mutating func setDateToTodayIfNeeded() {
        dateWrapper = dateWrapper ?? .init() // Note that 
    }

    var date: Date {
        dateWrapper.wrappedValue
    }
}

You can avoid writing all the coding keys if you rename the Date property instead, and name the DefaultTodayDate to be date.

To decode, you just need to call setDateToTodayIfNeeded in addition:

var result = try! JSONDecoder().decode(MyModel.self, from: json)
result.setDateToTodayIfNeeded() // if date key is missing, the date when this is called will be used
print(result.date)

You can avoid doing setDateToTodayIfNeeded if you don't mind using a mutating get on date, which I find quite "disgusting":

var date: Date {
    mutating get {
        let wrapper = dateWrapper ?? DefaultTodayDate()
        dateWrapper = wrapper
        return wrapper.wrappedValue
    }
}
var result = try! JSONDecoder().decode(MyModel.self, from: json)
print(result.date) // if date key is missing, the date when you first get date will be used

Other options for parsing JSON with various date formats is DateDecodingStrategy.custom, which is also worth exploring. You would just go through all the anticipated formats and try them out one by one.

CodePudding user response:

You can achieve this by adding a new decode(_:forKey:) method to KeyedDecodingContainerProtocol (or KeyedDecodingContainer) that will automatically be used by default conformances of Decodable.

This method must take the .Type of your property wrapper as its first argument and return an instance of your property wrapper as well.

In your case, such an extension would look like this:

extension KeyedDecodingContainerProtocol { // KeyedDecodingContainer works too
    public func decode(_ type: DefaultTodayDate.Type, forKey key: Key) throws -> DefaultTodayDate {
        return try decodeIfPresent(type, forKey: key) ?? DefaultTodayDate(wrappedValue: Date())
    }
}

Then just add this initializer to your DefaultTodayDate type:

public init(wrappedValue: Date) {
    self.wrappedValue = wrappedValue
}

Your example that was failing now works correctly:

let json = #"{ "book": "test" }"#.data(using: .utf8)!
let result = try! JSONDecoder().decode(MyModel.self, from: json)
print(result.date) // 2022-09-08 08:16:33  0000
print(Date())      // 2022-09-08 08:16:33  0000
  • Related