Handling Codable Parsing Errors

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.

⚠️
Please note, that for the sake of this article, I'll only include the decoding part. If your model is fully 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.