Handling Codable Parsing Errors
Stop API calls break your Swift code and learn how to prevent parsing errors when working with enums and arrays.
Parsing data using the Decodable protocol can be easy but also annoying as it breaks on the smallest inconsistency or change in the API. For example if you use a property with an enum
, your decoding will fail if the API adds a new case to it that is unknown by your Swift code. There are various ways to handle this without requiring an app update or new code deployment to prevent killing parts of your project for the users.
Example
For this article, I will use a simple example of a News
model with a source
property which the app will use to display the desired icon.
enum NewsSource: String, Decodable {
case someKindOfCode = "skoc"
case swiftBlog = "swift"
}
struct News: Decodable {
let title: String
let excerpt: String
let source: NewsSource
}
[
{
"title": "Handling Codable Parsing Errors",
"excerpt": "...",
"source": "skoc"
},
{
"title": "Swift 6.1 Released"
"excerpt": "...",
"source": "swift"
},
{
"title": "What's new in SwiftUI for iOS 26",
"excerpt": "...",
"source": "hackingwithswift"
}
]
With the JSON above, the third item will cause a decoding error, because its source
value can't be mapped to the defined enum cases in our model.
Not a solution: Just make the property optional
First of all what is NOT a simple solution: Making source
optional. Just because the property is set to be optional, doesn't mean it will be nil
on any decoding error. The valid values are just extended by the support for a non-existent key or a null
value in JSON.
Codable
, please consider adding a custom encoding if necessary.Solution #1: Enum fallback case
The first solution would be to create a fallback case, maybe with an associated enum that contains the original value. The later would require some additional code to resolve the original string values.
Simple variant
enum NewsSource: String, Decodable {
case someKindOfCode = "skoc"
case swiftBlog = "swift"
case unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self)
// Try to use the raw value initializer.
// If that doesn't succeed, fallback to the unknown case
self = .init(rawValue: value) ?? .unknown
}
}
Associated value variant
Based on the simple variant from above, we could also add an associated value to the unknown case. With that we'll be able to debug, log or even display that value easily.
// Note the removed String conformance
enum NewsSource: Decodable {
case someKindOfCode
case swiftBlog
case unknown(String)
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(String.self)
switch value {
case "skoc":
self = .someKindOfCode
case "swift":
self = .swiftBlog
default:
self = .unknown(value)
}
}
}
Solution #2: Gracefully failed parsing
Another solution that I prefer is to drop incompatible data. With this solution you loose SOME data, but won't kill your feature entirely.
I created a wrapper that will drop all the elements that can't be decoded successfully, but still makes the decoding errors accessible. With this solution, the news from a new source wouldn't be displayed until we got an update, but it will still display the other items.
You can use it in two ways.
Array replacement
LossySequence
is still conforming to the Sequence
protocol and can mostly be used as a replacement for an array and your decoding would look like this:
let news = try decoder.decode(LossySequence<News>.self, from: data)
news.errors // contains an array of errors that happened during decoding
news.forEach { singleNews in
// do sth with singleNews
}
Property Wrapper
The easiest way is to use it as a property wrapper.
Image we have a little different response from our API that has the news items nested in it:
{
"news": [],
"paging": {}
}
The corresponding model could look like this:
struct NewsResponse: Decodable {
@LossySequence
var news: [News]
let paging: Paging
}
To access the errors, you now have to access the projected value using $
:
let response = try JSONDecoder().decode(NewsResponse.self, from: data)
response // NewsResponse
response.news // Now a real array
response.$news // The LossySequence object
response.$news.error // The error array
Summary
Both ways are legit solutions and depend on your desired outcome. I personally mix both of them. For our news example, solution #1 would be better, as we would simply need a fallback UI for the .unknown
case. But as we could encounter many other sources for decoding errors, solution #2 could be considered as well and used in all the places where you have to decode a list of data.