How does synthesised Decodable treat missing keys, nulls, default values and number types?
Predict first
What does this print?
struct User: Codable { var id: Int; var name: String; var age: Int?; var tags: [String] = [] }
func attempt(_ json: String) {
do {
let u = try JSONDecoder().decode(User.self, from: Data(json.utf8))
print("ok", u)
} catch let e as DecodingError {
switch e {
case .keyNotFound(let k, _): print("keyNotFound", k.stringValue)
case .typeMismatch: print("typeMismatch")
case .valueNotFound: print("valueNotFound")
case .dataCorrupted: print("dataCorrupted")
@unknown default: print("other")
}
} catch { print("non-decoding error") }
}
attempt(#"{"id":1,"name":"a","tags":["x"]}"#)
attempt(#"{"id":1,"name":"a"}"#)
attempt(#"{"name":"a","tags":[]}"#)
attempt(#"{"id":1,"name":"a","age":null,"tags":[],"extra":true}"#)
attempt(#"{"id":1,"name":null,"tags":[]}"#)
attempt(#"{"id":1,"name":"a","tags":null}"#)
attempt(#"{"id":"1","name":"a","tags":[]}"#)
attempt(#"{"id":1.0,"name":"a","tags":[]}"#)
attempt(#"{"id":1.5,"name":"a","tags":[]}"#)
let e = JSONEncoder(); e.outputFormatting = .sortedKeys
print(String(data: try e.encode(User(id: 1, name: "a", age: nil)), encoding: .utf8)!)
- ok ... tags ["x"] / keyNotFound tags / keyNotFound id / ok age nil tags [] / valueNotFound / valueNotFound / typeMismatch / ok (1.0 accepted) / dataCorrupted / {"id":1,"name":"a","tags":[]}
- ok / ok tags [] / keyNotFound id / ok / valueNotFound / valueNotFound / typeMismatch / ok / ok / {"id":1,"name":"a"}
- ok / keyNotFound tags / keyNotFound id / ok / keyNotFound name / keyNotFound tags / typeMismatch / typeMismatch / typeMismatch / {"id":1,"name":"a","tags":[]}
Mental model. A form where a printed default is only a suggestion on your side: the other side still rejects an empty box.
The synthesised init(from:) ignores property defaults: var tags: [String] = [] is still required, so a payload without tags throws keyNotFound (and one without id does too). Only optional properties use decodeIfPresent, so a missing or null age becomes nil. A null for a non-optional field (name or tags) is valueNotFound, a string where an Int is expected is typeMismatch, and unknown extra keys are ignored. Encoding omits a nil optional but writes "tags":[].
In this Foundation 1.0 decodes into an Int while 1.5 fails with dataCorrupted (that is Foundation's number handling; do not treat it as language-level). Fix defaults with a custom init(from:) using try c.decodeIfPresent([String].self, forKey: .tags) ?? [], or make the property optional (Q54, Q102).
Defaults are ignored by synthesis, optionals are decodeIfPresent.
Common traps
- Trusting a default value to fill a missing key. It does not.
- Treating null and missing as the same for non-optionals. One is keyNotFound, the other valueNotFound.
They might follow up with
- How do you decode leniently? Q102.
Related questions
- 1.10.1 How does Codable synthesis work? When do you write CodingKeys or a custom init(from:)?
- 2.5.4 How do you read a DecodingError, and how do you decode tolerantly?
- 2.5.1 What does keyDecodingStrategy do, and when does convertFromSnakeCase not match?
← Which method runs through a protocol extension: when is a call… Where do a nonisolated async function, @concurrent, @MainActor and… →