Hi Stephen, thanks for putting this together! Some thoughts:
To clarify here, the default value doesn't override what's in the JSON; understanding what's happening here at the base level might put this behavior into more context.
Let's write out the implementation of User ourselves for a sec, because that's all that Codable synthesis does:
struct User : Decodable {
let id: Int
let name: String = ""
private enum CodingKeys : String, CodingKey {
case id, name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
}
}
If we try to compile this, we'd immediately see the error:
Untitled 3.swift:12:14: error: immutable value 'self.name' may only be initialized once
name = try container.decode(String.self, forKey: .name)
^
Untitled 3.swift:3:9: note: initial value already provided in 'let' declaration
let name: String = ""
^
Untitled 3.swift:3:5: note: change 'let' to 'var' to make it mutable
let name: String = ""
^~~
var
The property is immutable and already has a value set. We cannot override it no matter how much we want to. Codable synthesis, then, doesn't assign to the property, because it can't.
This is a little bit reversed — the idea is that if you don't want to encode or decode a value from your payload, you have to omit it from your CodingKeys. In order for the property to be initialized on decode, though, it must then have a default value.
Yes — the idea here is that encoding User would encode only its id and not its name.
I would consider the fact that this compiles without warning a bug. If your type is strictly Decodable and you have an immutable property with a default value and that value is included in the CodingKeys enum, I think we should warn about it. The inclusion of name here isn't helpful and hides the actual behavior.
We can and should improve this behavior if it's causing confusion, as long as we can do so in a backwards-compatible way:
This I think would be a bit too harsh. This is valid behavior and it's perfectly valid to rely on it doing the right thing.
Perhaps — however, it's reasonable to expect to want to encode the value if the type is Codable.
We can't get around the fact that the variable is immutable — having more complex behaviors around something that already appears "magic" enough I think could leave us in a worse spot.
I think one thing we could do is the following — for types with immutable properties that have default values, warn if:
- No explicit
CodingKeys are given (i.e. you're getting the default behavior without necessarily knowing about it)
- The value is exclusively
Decodable and the value appears explicitly in the CodingKeys (i.e. you're including something which won't get decoded)
I think case (1) is significantly more common, and we could provide a fix-it:
- If you expect to decode the value, make it mutable (at least
private(set))
- Provide an explicit
CodingKeys enum (with the value in it if the type is also Encodable, with the value not in it if the type is only Decodable)
The dangerous thing is having this happen to you implicitly without knowing about it; being informed of what's going on would help, I think.