Apologies for the delayed response here!
The half-baked idea I had had in mind when suggesting this solution is that when a property wrapper is Codable
, the compiler synthesis uses its underlying wrappedValue
for encoding and decoding — which means that if you could convert your MyOptional<Wrapped>
into a wrappedValue
of type Wrapped?
, the compiler would use that instead, and get you the behavior you want. e.g., something like
protocol OptionalConvertible {
associatedtype Wrapped
var optionalValue: Wrapped? { get set }
init(_ optionalValue: Wrapped?)
}
@propertyWrapper
struct OptionalCodable<T: OptionalConvertible>: Codable where T.Wrapped: Codable {
var wrappedValue: T.Wrapped? {
get { storage.optionalValue }
set { storage.optionalValue = newValue }
}
var storage: T
init(storage: T) {
self.storage = storage
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
storage = .init(try container.decode(Optional<T.Wrapped>.self))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
Then, with a custom type like
enum MyOptional<Wrapped>: OptionalConvertible {
case none
case some(Wrapped)
case fileNotFound
init(_ optionalValue: Wrapped?) {
switch optionalValue {
case .none: self = .none
case .some(let v): self = .some(v)
}
}
var optionalValue: Wrapped? {
get {
switch self {
case .none, .fileNotFound: return nil
case .some(let v): return v
}
}
set {
self = .init(newValue)
}
}
}
I envisioned that you could write
struct S {
@OptionalCodable(storage: MyOptional<Int>.none)
var field: MyOptional<Int>
}
However, this doesn't work, because the compiler enforces that the type of a property and its property wrapper's wrappedValue
must be the same type:
struct S {
@OptionalCodable(storage: MyOptional<Int>.none)
var field: MyOptional<Int> // 🛑 Property type 'MyOptional<Int>' does not match 'wrappedValue' type 'MyOptional<Int>.Wrapped?'
}
This could work if Codable synthesis looked up whether the property wrapper had a projectedValue
and used that, but it doesn't currently, and this might be a behavior-breaking change.
Edit: I actually think projectedValue
is unlikely to be useful; besides the fact that property wrappers can have projected values completely unrelated to the underlying type, you can get into various ambiguous scenarios with property wrapper composition. If you compose multiple property wrappers, Codable synthesis currently follows wrappedValue
s all the way down to the base type, but if you stop to look at projectedValue
branches along the way, it gets less clear what the end result would be. (And if a client reorders property wrappers, they may unexpectedly get completely different results.)
Someone more clever than I may find a way around this with property wrappers, but this is what I had in mind, at least. Sorry it didn't work out to help you.