Library / SDK
airbnb/ResilientDecoding avatar
airbnb/ResilientDecoding

ResilientDecoding: partial Swift decoding that keeps bad fields from killing the payload

This package makes your Decodable types resilient to decoding errors and allows you to inspect those errors.

602 stars27 forksSwiftMIT

At a glance

What is it?
Airbnb's ResilientDecoding adds a @Resilient property wrapper so a single malformed field no longer throws away a whole decoded model. It trades strictness for partial data, and it keeps the debugging aids out of release builds.
Who is it for?
Use ResilientDecoding when a schema you do not fully control can drift, such as a backend API consumed by a mobile client, and when a partially populated model is more useful than an error. Skip it when a silent fallback would hide a real contract violation, since the wrapper will not tell you a required field was missing in a release build without wiring up ResilientDecodingErrorReporter.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 122 days ago.
What is it written in?
Mainly Swift, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

ResilientDecoding is for payloads that drift out of your control

ResilientDecoding exists for the case where a server starts sending a shape your app did not expect and one wrong field should not blank the whole screen. The package is aimed at Swift engineers who decode `Decodable` types from services they do not fully control, and who would rather keep a partially valid object than handle a thrown `DecodingError` for every field. Its main interface is the `@Resilient` property wrapper, and the README frames the goal as partially recovering from errors while decoding `Decodable` types. Instead of failing the whole decode, a resilient property substitutes a safe default when the key is missing, the type is unexpected, or a single element fails. That is a deliberate trade: you give up strictness per field and gain an instance that still carries the parts that did decode. The project is MIT licensed, its badges list watchOS, iOS, tvOS, macOS and Linux, and the last push was on 2026-06-23.

The wrapper accepts four property shapes and swallows a different mismatch in each

Resilience is applied per property. The wrapper can be applied to `Optional`, `Array`, `Dictionary`, and custom types conforming to `ResilientRawRepresentable`, and each shape absorbs a different kind of mismatch. The README gives this example:

swift
struct Foo: Decodable {
  @Resilient var array: [Int]
  @Resilient var value: Int?
}
let foo = try JSONDecoder().decode(Foo.self, from: """
  {
    "array": [1, "2", 3],
    "value": "invalid",
  }
  """.data(using: .utf8)!)

The result of that snippet is the clearest statement of the behaviour the package is built around.

A bad array element disappears while a bad optional becomes nil

After running the example, `foo.array == [1, 3]`, because the string element in the middle could not be decoded and was omitted, and `foo.value == nil`, because the value for that key was a string where an integer was expected. A plain optional property written `@Resilient var foo: Int?` initializes to `nil` and does not throw when the encoded value has the wrong type. A non optional array property initializes to an empty array when the key is missing or the value is something unexpected such as a `String`, and any element that fails to decode is dropped. The optional array variant sets the value to `nil` when the key is missing or null and an empty array in every other failure case. A dictionary behaves the same way, except that it is the values that are dropped when one of them fails to decode.

Adding the package through Swift Package Manager or CocoaPods

Installation is a single dependency entry in either of the two package managers the README covers. With Swift Package Manager, add the package to the dependencies of your `Package.swift` file:

swift
  dependencies: [
    .package(name: "ResilientDecoding", url: "https://github.com/airbnb/ResilientDecoding.git", from: "1.0.0"),
  ]

For a CocoaPods based project, the README shows a Podfile with an explicit platform declaration and the pod pinned to the 1.0 series:

ruby
platform :ios, '12.0'
pod 'ResilientDecoding', '~> 1.0'

Nothing else is required to get started, since the wrapper is applied at the property declaration and the rest of your `Decodable` conformance stays as it was.

decodingFallback applies to a bare enum but never inside a collection

Custom types can conform to `ResilientRawRepresentable`, which inherits from `RawRepresentable` and exists to change behaviour when a value is decoded as a `Resilient` property. The README notes it has no effect otherwise. The protocol has two static properties, `decodingFallback` and `isFrozen`, and the fallback lets an enum decode without being wrapped in an optional:

swift
enum MyEnum: String, ResilientRawRepresentable {
  case existing
  case unknown
  static var decodingFallback: Self { .unknown }
}

The sharp edge here is documented plainly: arrays and dictionaries of `ResilientRawRepresentable` values always omit elements instead of using the fallback, so a fallback case never shows up inside a collection.

isFrozen suppresses reports for enum cases your build has never seen

The fallback above is only half of the protocol. `isFrozen` controls whether a new raw value reports an error to `ResilientDecodingErrorReporter`, and it defaults to `false`, meaning an unknown raw value is accepted silently. The intended use is an evolving backend API: an older build of your app can tolerate a new enum case without reporting an error every time it meets one. The README compares the property to Swift's `@frozen` attribute while noting that the two achieve different goals, and it states that `isFrozen` has no effect on property level errors. That combination is worth thinking about before you lean on it, because the flag only quiets the production reporter while a bad value still changes what your model contains.

Error inspection lives in DEBUG, and the reporter lives in production

The package offers two separate mechanisms for looking at what went wrong, and the split matters for release builds. In a `DEBUG` build, a resilient property exposes a projected value with information about decoding errors, readable through `$property.outcome`, an enum with cases such as `keyNotFound` and `valueWasNil`. Scalar types also expose `$foo.error`, while array properties add `errors`, the list of recovered failures, and `results`, which interleaves successes and failures so you can see exactly where a gap appeared. The README states this functionality is `DEBUG` only, so that release builds carry no overhead, and it links the memory tests that back that claim. For production, `ResilientDecodingErrorReporter` collates the errors encountered while decoding a type, and `JSONDecoder` exposes a `decode(_:from:reportResilientDecodingErrors:)` API that returns the decoded value together with an error digest when errors were found. More complex setups add a reporter to the decoder directly.

The strict decoder is the better answer when a missing field is a bug

ResilientDecoding is the wrong tool for a payload whose contract you own and can enforce. If a required identifier is missing, a silent `nil` or an empty collection removes the failure signal exactly where you need it. The wrapper also only covers four property shapes, so anything outside that set still throws, and a partially invalid list can come back shorter than the server sent. The diagnostic layer is `DEBUG` only, so a release build that never wires up `ResilientDecodingErrorReporter` will quietly accept bad data. Versioning also deserves attention: the last tagged release listed here is 1.2.0 from 2024-09-28, the previous tags are 1.1.0 from 2020-12-02 and 1.0.0 from 2020-04-27, and the most recent push was on 2026-06-23, so the gap between the newest tag and the newest commit is worth checking before you pin a version.

Editorial conclusion

Use ResilientDecoding when a schema you do not fully control can drift, such as a backend API consumed by a mobile client, and when a partially populated model is more useful than an error. Skip it when a silent fallback would hide a real contract violation, since the wrapper will not tell you a required field was missing in a release build without wiring up ResilientDecodingErrorReporter. Before adopting, check the exact property types you plan to mark resilient, confirm that omitting a bad array element is acceptable for your data, and pin the version you test against, since the last tagged release was 1.2.0 on 2024-09-28 while the last push was on 2026-06-23.

Frequently asked questions

How do I add ResilientDecoding to a Swift project?

The README shows two routes. With Swift Package Manager, add a package entry for `https://github.com/airbnb/ResilientDecoding.git` from version `1.0.0` to your `Package.swift` dependencies, or add `pod 'ResilientDecoding', '~> 1.0'` to your Podfile.

What happens to a ResilientDecoding array when one element cannot be decoded?

That element is omitted and the rest of the array is kept, so `[1, "2", 3]` decodes to `[1, 3]`. In a `DEBUG` build, the `$property.errors` list holds the recovered failures and `$property.results` shows successes and failures interleaved.

Does ResilientDecoding report decoding errors in release builds?

Property level inspection through `$property.outcome` and `$property.error` is `DEBUG` only, which the README ties to keeping overhead out of release builds. In production, `ResilientDecodingErrorReporter` is used to collate errors, and `JSONDecoder` offers `decode(_:from:reportResilientDecodingErrors:)` to return an error digest alongside the value.

How do I give an enum a fallback value when using ResilientDecoding?

Conform the enum to `ResilientRawRepresentable` and define `static var decodingFallback: Self` with the case to use, then mark the property `@Resilient`. The README notes that arrays and dictionaries of such enums always omit unknown elements instead of applying the fallback.

What is the difference between decodingFallback and isFrozen in ResilientDecoding?

`decodingFallback` supplies the value used when a raw value cannot be created, while `isFrozen` decides whether an unknown raw value is reported to `ResilientDecodingErrorReporter`. By default `isFrozen` is `false`, so unknown cases are accepted without a report, which suits older app versions meeting new enum cases.

Official sources

  1. airbnb/ResilientDecoding on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/airbnb-resilientdecoding.svg)](https://hysenlabs.com/projects/airbnb-resilientdecoding)

Was this useful?