Ah, thanks for the example. Codable
synthesis does use decodeIfPresent
for all optional properties, but importantly, annotating a variable with a property wrapper creates a new property with a different underlying type.
When you write
@Crypto var name: String?
the compiler produces the equivalent of
private var _name: Crypto<String?>
var name: String? {
get { return _name.wrappedValue }
set { _name.wrappedValue = newValue }
}
In Codable
synthesis in the compiler, wrapped properties are treated specially: when the compiler finds the name
property, it looks up the backing property for the wrapper:
static std::tuple<VarDecl *, Type, bool>
lookupVarDeclForCodingKeysCase(DeclContext *conformanceDC,
EnumElementDecl *elt,
NominalTypeDecl *targetDecl) {
for (auto decl : targetDecl->lookupDirect(
DeclName(elt->getBaseIdentifier()))) {
if (auto *vd = dyn_cast<VarDecl>(decl)) {
// If we found a property with an attached wrapper, retrieve the
// backing property.
if (auto backingVar = vd->getPropertyWrapperBackingProperty())
vd = backingVar;
// ... use `vd` to look up the type of the property, and use that
// for synthesis.
This means that for the purposes of encoding and decoding, the compiler will encode and decode _name
, not name
. This is crucial, because it's what allows Crypto
to intercept encoding and decoding of the property with its own Codable
conformance.
But it's important to note that the type of _name
is Crypto<String?>
, which is not Optional
. You can see this if you try to implement init(from:)
on Item
the same way that you expect the compiler to:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// `name` is a computed property, which can't be assigned to in an initializer.
// You have to write to `_name`, which is the real property.
_name = try container.decodeIfPresent(Crypto.self, forKey: .name)
// ❌ error: Value of optional type 'Crypto?' must be unwrapped to a value of type 'Crypto'
}
If you want to write this, you'll need to support some type of fallback:
struct Item: Codable {
@Crypto var name: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_name = try container.decodeIfPresent(Crypto.self, forKey: .name) ?? Crypto(value: nil)
}
}
This does work the way that you might expect:
let data = """
{}
""".data(using: .utf8)!
let item = try! JSONDecoder().decode(Item.self, from: data)
print(item) // => Item(_name: main.Crypto(value: nil))
While this behavior is somewhat pedantically correct, the synthesis behavior might not be terribly useful. Theoretically, the compiler could be augmented to do this automatically — it can look through the type of the property wrapper to see if the contained type is Optional
, and use a decodeIfPresent
then, though this does get complicated in the face of multiple nested wrappers. The biggest risk would be the behavior change.
This might be worth filing as a bug for consideration.