Implementing init(from:)
yourself is currently the only way to handle this, since neither decode(_:forKey:)
nor decodeIfPresent(_:forKey:)
will do what you want:
decode(_:forKey:)
will throw an error if the key is missingdecodeIfPresent(_:forKey:)
won't distinguish between a missing key and anull
value
Distinguishing between non-existent key and a null
value requires using contains(_:)
, and you'd need to decide how to handle the result. This, for example, is one way to represent the data (but not the only way):
struct S: Decodable {
// Double-Optional:
// * Outer layer indicates key missing vs. present
// * Inner layer indicates value `null` vs. present
var x: Int??
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.x) {
x = container.decode(Int?.self, forKey: .x)
} else {
x = nil
}
}
}
FWIW, if your type is Encodable
too, you could leverage the fact that letting the compiler synthesize Encodable
conformance will generate the keys for you, and then use those keys in your init(from:)
.
Unfortunately, you can't handle this with a property wrapper because of how property wrappers are represented under the hood, and how they interact with compiler synthesis.
A type like
struct S {
@MyPropertyWrapper var x: Int?
}
generates code that looks approximately like
struct S {
private var _x: MyPropertyWrapper<Int?>
var x: Int? {
get { _x.wrappedValue }
set { _x.wrappedValue = newValue }
}
}
This produces an init(from:)
that looks like
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_x = try container.decode(MyPropertyWrapper<Int?>.self, forKey: .x)
}
Because of how the synthesized code is structured, an error would get thrown on a missing key before the property wrapper code would ever get called, one level deeper.
(A bit more info on this in Cleaner Way to Decode Missing JSON Keys? - #7 by itaiferber and [Pitch] Nullable and Non-Optional Nullable Types - #17 by itaiferber)
There's also quite a bit of prior discussion in threads like