SmartCodable: decoding Swift models that survive messy JSON
SmartCodable is a data parsing library built on Swift’s Codable, designed for simple usage and strong real-world compatibility. It gracefully handles missing fields, default values, and evolving JSON structures. SmartCodable 是基于 Swift Codable 的数据解析库,主打简单易用与真实业务场景下的强兼容性,能够优雅应对不断变化的 JSON 数据。
At a glance
- What is it?
- SmartCodable is a Swift package that layers resilience on top of Apple's Codable, so a missing key, a string where an integer was expected or a null for a non-optional property no longer costs you the whole model. It is aimed at apps consuming APIs that change under them, and version 7 has moved to Swift Package Manager only.
- Who is it for?
- SmartCodable suits iOS and macOS apps that decode third-party JSON that changes without notice, and the recovery behaviour is the whole point, since a single renamed field otherwise fails the entire parse. It does not suit a codebase that wants strict decoding, because the library's purpose is to keep going where Codable would stop, and the same resilience hides genuinely wrong data.
- 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 29 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
The failure mode SmartCodable is built to avoid
The pitch in one sentence: when standard Codable fails on a single missing field or type mismatch, your entire model is lost, while SmartCodable falls back to defaults, converts types automatically and never interrupts the parse. The README puts the contrast in a table, and the four rows are the whole argument.
A missing key makes Codable throw keyNotFound and the model fails. SmartCodable uses the property initializer as the default instead. A type mismatch, such as the string "123" arriving where an Int is expected, makes Codable throw typeMismatch; SmartCodable auto-converts and returns 123. A null value for a non-optional property makes Codable throw valueNotFound; SmartCodable falls back to the default value. Extra unknown keys are ignored by both, so that row is a tie.
The design consequence is that your model stops being a schema assertion and becomes a best-effort reader. That is a real gain when the data comes from a service you do not control, and a real loss when you want a strict contract, because a silently defaulted field is indistinguishable at the call site from a correctly parsed one.
The library sits on Apple's Codable protocol rather than replacing it. The README contrasts it with HandyJSON, which relies on Swift metadata reflection that may break across Swift versions, so SmartCodable claims no unsafe runtime reflection and no ABI stability risk. It also positions itself against writing a manual init(from:) with decodeIfPresent and a fallback for every property, which is the same behaviour with more boilerplate.
Getting started: the deserialize call in three situations
Adopting the library is a protocol conformance and one static call. A struct declares SmartCodableX and gives every property a default.
import SmartCodable
struct User: SmartCodableX {
var name: String = ""
var age: Int = 0
}The three cases from the README follow directly.
let user = User.deserialize(from: ["name": "John", "age": 30])
let user2 = User.deserialize(from: ["name": "John"])
let user3 = User.deserialize(from: ["name": "John", "age": "30"])The first call returns User(name: "John", age: 30). The second, with the age key absent, returns User(name: "John", age: 0), because the property initializer supplies the value. The third, with the age arriving as the string "30", returns User(name: "John", age: 30), because the type is converted. Nothing throws and nothing returns nil in any of the three.
Classes need one extra step, an empty initializer, since the compiler cannot synthesise one for a class with no stored defaults of its own.
class BasicTypes: SmartCodableX {
var int: Int = 2
var doubleOptional: Double?
required init() {}
}
let model = BasicTypes.deserialize(from: json)For a struct the compiler provides the default empty initializer, so the same class body without the init line is enough.
Installing with Swift Package Manager, and what happened to CocoaPods
The package is installed through Swift Package Manager, and the dependency declaration in the README is short.
dependencies: [
.package(url: "https://github.com/iAmMccc/SmartCodable.git", branch: "main")
]Two notes come with it. The SmartCodable package provides the core parsing capabilities with no external dependencies, and for class inheritance through the @SmartSubclass attribute the README points to a companion package, SmartCodableMacro, which is a separate repository.
The CocoaPods story is the part worth reading carefully. Starting from 7.0 the library no longer ships via CocoaPods, and if you must stay on CocoaPods you are told to use the 6.x series, which is preserved on the 6.1.0 branch.
pod 'SmartCodable', '~> 6.1'The 6.x line will receive critical fixes only, all new features land in 7.x and beyond, and the README recommends migrating to Swift Package Manager. In practice this means a CocoaPods shop is pinned to a frozen feature set while the active development happens elsewhere, which is a migration deadline in everything but name.
Input formats, deep paths and decoding strategies
The deserialization entry points are static functions on the conforming type, one per input shape, each accepting an optional designatedPath and an optional set of SmartDecodingOption. There is a variant taking a dictionary, one taking a JSON string, one taking Data, and one named deserializePlist for property list data. Only types conforming to SmartCodable, or arrays of such types, can use them.
The input table in the README shows how each shape is handled internally. A dictionary and an array are processed directly as native collections, a JSON string is converted to Data via UTF-8, and Data is processed directly.
The designatedPath argument extracts nested data without hand-rolling a path through dictionaries.
let model = Model.deserialize(from: json, designatedPath: "data.user.info")Strategies are passed as a set of options.
let options: Set<SmartDecodingOption> = [
.key(.convertFromSnakeCase),
.date(.iso8601),
.data(.base64)
]
let model = Model.deserialize(from: json, options: options)Five strategy families are listed. Key decoding offers .fromSnakeCase, .firstLetterLower and .firstLetterUpper. Date decoding exposes the full set of Codable date strategies, with .iso8601 and .secondsSince1970 named. Data decoding has .base64. Float decoding covers NaN and infinity handling with .convertToString and .throw. The README warns that only one strategy per type is allowed, and that the last one wins if you pass duplicates.
Key mapping is the other half of the configuration. A static mappingForKey() function returns an array of SmartKeyTransformer values that pair a CodingKeys case with one or more alternative JSON keys, and the first non-null match wins, so the order of the alternatives is meaningful. The example in the README maps an id key to a list of user_id, userId and id, and a name key to a dotted nested path.
Where SmartCodable is the wrong choice
The first limitation is the one the library is built on. Every recovery path is a place where a real error is hidden. A field that arrives as a string is coerced, a field that disappears is defaulted, and a field that arrives null is defaulted again. If your API contract is stable and your models are meant to be strict, you have traded a crash for a wrong number that nobody notices until a report is wrong.
Second, the type conversion is a guess. The README shows "30" becoming 30, but it does not specify what happens for a value that cannot be converted at all, and it does not say whether the conversion is applied recursively through nested models or only at the top level. The type mismatch table describes a happy path, not the full contract.
Third, the strategy rule is a trap. One strategy per type, last one wins, means passing .iso8601 and .secondsSince1970 in the same set silently discards one. The behaviour is documented but the failure is invisible at the call site, and it will bite a team that assumes both formats are supported.
Fourth, the CocoaPods split. If your project is still on CocoaPods, you are on 6.x with critical fixes only, which means a new server field or a new Swift version can leave you without a path forward short of migrating the whole build system.
Fifth, the runtime reflection question is settled by avoiding it, but not for free. Building on Codable means your models must express their schema through Codable-compatible types, and the mapping and strategy machinery is configured with static functions and options, which is more ceremony than a plain init(from:) but less than a hand-rolled mapping layer.
Licence, releases and the cost of upgrading across the 6.x to 7.x jump
The project is under the MIT licence, with the LICENSE file at the root, and the primary language is Swift with a stated minimum of Swift 5.0.
The release history shows a major version roughly yearly with frequent patches inside it. Recent tags are 7.0.1 on 2026-09-24, a 7.0.1 beta on 2026-09-17 and 6.0.1 on 2026-01-22. The repository's last push was on 2026-09-24, the same day as the 7.0.1 tag, so the project is not dormant.
The 6.x to 7.x jump carries two separate costs that are easy to confuse. The distribution change is the loud one, moving from CocoaPods to Swift Package Manager, with a preserved 6.1.0 branch and critical fixes only for the old line. The behavioural change is quieter, and it is the one to test for. A library whose job is to decide what happens when the data does not match the model has version-specific decisions about defaults, coercions and strategy precedence, and those decisions can change between a major version without any compiler error to warn you.
So the upgrade path is: move to Swift Package Manager first, then treat the 6.x to 7.x behaviour change as a deliberate step with your own decoding tests rather than a routine dependency bump. The repository ships a Tests/ directory and an Example/ExampleApp/ target, so the behaviour is testable in the package itself.
Editorial conclusion
SmartCodable suits iOS and macOS apps that decode third-party JSON that changes without notice, and the recovery behaviour is the whole point, since a single renamed field otherwise fails the entire parse. It does not suit a codebase that wants strict decoding, because the library's purpose is to keep going where Codable would stop, and the same resilience hides genuinely wrong data. Verify first that your target SDK supports Swift 5.0 or above and that you are on the 7.x line, since CocoaPods support ended at 7.0 and the 6.x branch now takes critical fixes only.
Frequently asked questions
How do I make a Swift model decode JSON with missing fields using SmartCodable?
Declare the type as conforming to SmartCodableX and give every property a default value, then call the static deserialize method. A struct gets an empty initializer from the compiler, while a class has to implement a required init() with an empty body.
Does SmartCodable support CocoaPods?
Not from version 7.0. The README says to use the 6.x series if you must stay on CocoaPods, preserved on the 6.1.0 branch, and that the 6.x line receives critical fixes only while new features land in 7.x and beyond.
How do I decode a nested object with SmartCodable?
Pass the designatedPath argument to the deserialize call, for example designatedPath: "data.user.info" for a JSON shaped as {"data": {"user": {"info": { ... }}}}. The README also supports key mapping through a static mappingForKey() function that pairs CodingKeys with alternative key names.
What is the difference between SmartCodable and HandyJSON?
SmartCodable builds on Apple's Codable protocol with no unsafe runtime reflection, while HandyJSON relies on Swift metadata reflection that the README says may break across Swift versions. SmartCodable is also positioned as an alternative to writing init(from:) with decodeIfPresent and a fallback for every property.
Official sources
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.
[](https://hysenlabs.com/projects/iammccc-smartcodable)Was this useful?