Currently, adding a default value to a decodable struct will always override the value, even if it appears in the original JSON.
import Foundation
struct User: Decodable {
let id: Int
let name: String = ""
}
let decoder = JSONDecoder()
let user = try! decoder.decode(User.self, from: Data("""
{
"id": 1,
"name": "Blob"
}
""".utf8))
user.name // ""
This might be surprising to folks on this forum! It was surprising to me even after using Codable
a lot.
The behavior is partially documented, and kinda quietly: it's a feature that allow for structs to synthesize init(from decoder:)
and omit certain CodingKeys
when a default value exists.
A property omitted from
CodingKeys
needs a default value in order for its containing type to receive automatic conformance toDecodable
orCodable
.
(From "Choose Properties to Encode and Decode Using Coding Keys.")
For example, the following compiles just fine:
struct User: Decodable {
let id: Int
let name: String = ""
enum CodingKeys: String, CodingKey {
case id
}
}
This becomes more confusing, though, when you add an explicit coding key for a property with a default.
struct User: Decodable {
let id: Int
let name: String = ""
enum CodingKeys: String, CodingKey {
case id
case name
}
}
let decoder = JSONDecoder()
let user = try! decoder.decode(User.self, from: Data("""
{
"id": 1,
"name": "Blob"
}
""".utf8))
user.name // ""
This is a runtime bug waiting to happen. I think the compiler should be a bit more strict here (rather than have an opinionated, sometimes confusing default). Let's brainstorm some more reasonable expectations around synthesized init(from decoder:)
and structs with default property values. I can picture a few solutions to the problem:
- Never synthesize
init(from decoder:)
for structs with default property values. Harsh but no gotchas. - Only synthesize when an explicit set of
CodingKeys
are declared and the property with a default value is omitted. - Synthesize when an explicit set of
CodingKeys
are declared, with different behavior depending on whether or not the property is omitted:- If it's omitted, always use the default value.
- If it's present, fall back to the default value if the property is
null
or missing.
- The same as 3., but additionally synthesize when no
CodingKeys
are declared, changing the behavior to override default property values when JSON is present at that key and non-null
.
For 3. and 4. there's some details to work out around optionality, but I imagine the only sensible way to handle the optional property cascade is to:
- If the key exists in the JSON and is
null
, it should benil
. - If the key is omitted in the JSON, it should use the default.
E.g.:
struct User: Decodable {
let name: String? = "Blob"
}
let decoder = JSONDecoder()
let user1 = try! decoder.decode(User.self, from: Data("""
{
"name": null
}
""".utf8))
user1.name // nil
let user2 = try! decoder.decode(User.self, from: Data("{}".utf8))
user2.name // Optional("Blob")
Thoughts?