PropertyWrapper不存在的可编码属性的默认值



我创建了一个属性包装器,如下所示:

@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)
}
}

和一个类似的模型:

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

所以,如果我想解析这个json文件,一切都可以:

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

但我也想解析一个没有date属性的json,但我得到了。致命错误:

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

您可以通过向KeyedDecodingContainerProtocol(或KeyedDecodingContainer(添加一个新的decode(_:forKey:)方法来实现这一点,该方法将在默认符合Decodable的情况下自动使用。

此方法必须将属性包装器的.Type作为其第一个参数,并返回属性包装器实例。

在您的情况下,这样的扩展看起来是这样的:

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())
}
}

然后只需将此初始值设定项添加到DefaultTodayDate类型:

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

您失败的示例现在可以正常工作:

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

根据属性包装器的建议,用属性包装器注释的属性将被转换为计算属性和存储属性。存储的属性存储属性包装器的实例,计算的属性获取(或设置(包装的值。

例如,这是一种变体:

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

请注意,在所有变体中,存储的属性始终是非可选的。这意味着,在生成Codable一致性时,属性包装器将始终生成为decode,而不是decodeIfPresent,并且如果密钥不存在,则会引发错误。

所以@DefaultTodayDate var date: Date是不可能的,但我们仍然可以使用DefaultTodayDate作为一个普通类型,假设您想使用包装器来解析它。

首先,在DefaultTodayDate:中添加一个无参数初始化程序

public init() {
wrappedValue = Date()
}

然后执行:

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
}
}

如果重命名Date属性,并将DefaultTodayDate命名为date,则可以避免写入所有编码键。

要解码,您只需要调用setDateToTodayIfNeeded即可:

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)

如果你不介意在date上使用mutating get,你可以避免使用setDateToTodayIfNeeded,我觉得这很"有趣";恶心":

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

用各种日期格式解析JSON的其他选项是DateDecodingStrategy.custom,它也值得探索。你只需要浏览所有预期的格式,然后逐一尝试。

最新更新