Codable errors are finally readable in Swift 6.3
Table of Contents
Here is a decoding failure most of us have scrolled past.
struct Person: Codable {
var name: String
var home: Home
}
struct Home: Codable {
var city: String
var country: Country
}
struct Country: Codable {
var name: String
var population: Int
}
Decode some JSON where a deeply nested field is missing:
// Note the missing "population" field
let jsonData = Data("""
[
{
"name": "Ada Lovelace",
"home": {
"city": "London",
"country": {
"name": "England"
}
}
}
]
""".utf8)
do {
_ = try JSONDecoder()
.decode([Person].self, from: jsonData)
} catch {
print(error)
}
Before Swift 6.3, that prints this — one unbroken line, wrapped here to fit the page:
keyNotFound(CodingKeys(stringValue: "population",
intValue: nil),
Swift.DecodingError.Context(codingPath:
[_CodingKey(stringValue: "Index 0",
intValue: 0), CodingKeys(stringValue: "home", intValue:
nil),
CodingKeys(stringValue: "country", intValue: nil)],
debugDescription: "No value associated with key
CodingKeys(stringValue: \"population\", intValue: nil)
(\"population\").",
underlyingError: nil))
Everything we need is in there. The kind of failure, the missing key, the path to it, the underlying error. It is just buried in enum-reflection output dense enough that plenty of people assume it is log spam and go add print statements instead.
SE-0489, implemented in Swift 6.3, conforms DecodingError and EncodingError to CustomDebugStringConvertible and gives them a description built for reading.
What it looks like now
Each of these comes from decoding into the same Person above. The error is one line; it is wrapped here to fit the page.
A key is missing. country has no population:
[{"name":"Ada","home":{"city":"London",
"country":{"name":"England"}}}]
DecodingError.keyNotFound: Key 'population' not found in
keyed decoding container. Path: [0].home.country. Debug
description: No value associated with key
CodingKeys(stringValue: "population", intValue: nil)
("population").
A value has the wrong type. population is a string:
"country":{"name":"England","population":"56 million"}
DecodingError.typeMismatch: expected value of type Int.
Path: [0].home.country.population. Debug description:
Expected to decode Int but found a string instead.
A value is null where the property is not optional:
"country":{"name":"England","population":null}
DecodingError.valueNotFound: Expected value of type Int
but found null instead. Path:
[0].home.country.population. Debug description: Cannot
get value of type Int -- found null value instead
The JSON is not valid at all:
[{"name": "Ada",]
DecodingError.dataCorrupted: Data was corrupted. Debug
description: The given data was not valid JSON..
Underlying error: Error Domain=NSCocoaErrorDomain
Code=3840 "Unexpected character ']' around line 1,
column 17."
Encoding fails in the same shape. Double.infinity has no JSON representation:
struct Reading: Codable { var celsius: Double }
try JSONEncoder().encode(Reading(celsius: .infinity))
EncodingError.invalidValue: inf (Double). Path: celsius.
Debug description: Unable to encode Double.inf directly
in JSON.
The shape is always the same: the error case, what went wrong in plain English, the path, and the decoder's own debug description — plus the underlying error when there is one.
Path: is the biggest win. [0].home.country.population replaces an array of CodingKey structs, and array indices read as indices instead of a key whose string value happens to be "Index 0". When the path is empty it is left out entirely, rather than printing codingPath: [].
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Two caveats
The format is not a contract. The proposal deliberately does not pin down the exact output, so read these strings, don't parse them. To detect a specific failure in code, keep switching over the DecodingError cases and inspecting Context.
It is not back-deployable. The conformance lives in the standard library, so on ABI-stable platforms it needs a new enough OS to be present — building with Swift 6.3 is not by itself sufficient. Older deployment targets keep the old output.
You can easily support sarunw.com by checking out this sponsor.
AI Grammar: Correct grammar, spell check, check punctuation, and parphrase.
Summary
Swift 6.3 conforms DecodingError and EncodingError to CustomDebugStringConvertible, so print(error) now gives you the error case, a readable explanation, a dotted coding path like [0].address.city.birds[1].name, and any underlying error, on one line.
Nothing in your code needs to change to get it. The same print(error) that produced the wall of text produces the readable line.
The format is deliberately unspecified, so treat it as something to read during debugging, not something to parse.
Read more article about Swift, Swift Evolution, Codable, Debugging, or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareTyped throws in Swift 6
Swift 6 lets a function declare exactly which error type it throws. Here is the syntax, what it does to catch blocks, and why untyped throws is still the right default.