Hey everyone ![]()
I'd like to share LenientCodable, a macro package for resilient Codable decoding.
GitHub: GitHub - EngOmarElsayed/swift-lenient-codable: Swift macros for resilient Codable — one unknown enum case or malformed array element no longer fails your whole response. Lenient by default, strict by explicit opt-in, with compile-time diagnostics and fix-its. · GitHub
Docs: LenientCodable Documentation
The problem
Swift's synthesized decoding is all-or-nothing. One surprise anywhere in the payload — the backend ships an enum case your compiled app doesn't know, one element in a 20-element array is malformed — and the entire response throws. The bug ships silently and detonates the day the API evolves, usually in the oldest app version still installed.
What it looks like
import LenientCodable
@LenientDecodable
struct ApplicationResponse {
@Strict var applicationId: String // decode fails if this fails
var status: Status? // lenient by default: nil on any failure
@NilOnFailure var documents: [Document?] // failed elements → nil in place
@DropOnFailure var offers: [Offer] // failed elements → removed
}
@LenientDecodable generates CodingKeys, init(from:), and the Decodable conformance. Unannotated properties are lenient by default — failures degrade into nil (or dropped elements) exactly where they happened, instead of sinking the whole decode.
The design guarantee
The part I care most about: nothing is silently strict and nothing is silently lenient.
Lenient-by-default requires the type to have a nil-shaped hole for the failure to land in (T?, [T?], [K: V?]…). A non-optional Int with no annotation is a compile error with fix-its offering your actual choices: change the type to Int?, or opt out explicitly with @Strict. Every property's failure behavior is readable at its declaration site, and the compiler enforces that the accounting is complete.
A consequence I've come to love: in a @LenientDecodable struct, @Strict properties are the only way a decode can fail — so grep @Strict audits every hard failure point in your models, and the compiler guarantees the list is exhaustive.
Leniency without silent data loss
Every absorbed failure is logged in DEBUG builds via os.Logger (missing keys included — the backend omitting a field is worth knowing about; an explicit JSON null is the one silent case). Release builds compile the logging out entirely. Production degrades gracefully; development stays loud.
Dictionaries are handled with their two distinct failure points (key vs. value), with a small LenientDictionaryKey protocol as the string → key contract — String, Int, and RawRepresentable keys work out of the box.
How it differs from prior art
ResilientDecoding (Airbnb) and BetterCodable solve overlapping problems with property wrappers. LenientCodable being macro-based changes a few things:
- No wrapper types in your stored properties →
Equatable/Hashable/memberwise-init synthesis are untouched - Compile-time shape validation with fix-its, instead of runtime-only behavior
- Lenient by default with enforced total accounting, rather than opt-in leniency per property
- Readable generated code — right-click → Expand Macro shows exactly what was written, including provenance comments on implicitly-lenient properties
Honest limitations
Structs only in v1 (classes and enums are a compile error), explicit type annotations required (macros see syntax, not inferred types), and leniency depth is one level — for control inside a nested type, make it @LenientDecodable too; leniency composes by nesting. And to be clear about intent: this is for API evolution, not hiding bugs — anything whose absence should be impossible (IDs, amounts in a payments flow) belongs on @Strict, and the README has a section on exactly that.
Requirements
Swift 6.2+ toolchain (Xcode 26+), deploys back to iOS 13 / macOS 10.15. v1.1.0 is out, MIT licensed, builds on all platforms including Linux, Wasm, and Android, with zero data race safety errors on SPI.
I'd love feedback — on the API surface, the lenient-by-default choice (I know defaulting to leniency is opinionated!), and edge cases you'd expect covered. Issues and PRs are very welcome.