Swift macros that make Codable decoding tolerant of missing or null JSON fields by applying compile-time default values, while leaving required properties strict. Custom JSON key names are supported via @Default(_:codingKey:) or a hand-written CodingKeys enum.
API responses often omit keys or send null for optional configuration fields. With plain Codable, you typically need manual init(from:), property wrappers, or post-decode merging. CodableDefault keeps models declarative: mark fields with @Default, attach @CodableDefault to the type, and the macro generates decoding logic for you.
| Component | Version |
|---|---|
| Swift | 6.2+ |
| Xcode | 16+ (recommended) |
| iOS | 13+ |
| macOS | 10.15+ (required to build and run macro tooling) |
Dependencies are pinned via Package.resolved (swift-syntax 602.x, up to next minor).
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/tomisacat/CodableDefault.git", from: "1.0.0"),
],
targets: [
.target(
name: "<YourTarget>",
dependencies: [
.product(name: "CodableDefault", package: "CodableDefault"),
]
),
]-
Open your app or workspace in Xcode.
-
Choose File → Add Package Dependencies…
-
Paste the repository URL:
https://github.com/tomisacat/CodableDefault.git -
Set the dependency rule (for example Up to Next Major from
1.0.0), then click Add Package. -
When prompted, add the CodableDefault library product to the target that contains your
Codablemodels (your app target or a framework target). -
In Swift files that use the macros, add:
import CodableDefault
- File → Add Package Dependencies… → Add Local…
- Select the folder that contains
Package.swift(the repo root), notSources/. - Add the CodableDefault library product to your app or framework target — not
CodableDefaultClient. - Build once (⌘B). The module appears in the index only after a successful build.
import CodableDefaultin files that use@CodableDefault/@Default.
Equivalent Package.swift dependency:
dependencies: [
.package(path: "../CodableDefault"), // path to this repo
],
targets: [
.target(
name: "<YourTarget>",
dependencies: [
.product(name: "CodableDefault", package: "CodableDefault"),
]
),
]This usually means Xcode has not built or linked the library yet — not that the import name is wrong.
- Link the product — In your app target → General → Frameworks, Libraries, and Embedded Content, confirm CodableDefault is listed. If you only added the package to the project without assigning it to a target, the module will not be available.
- Pick the library product — Add CodableDefault, not
CodableDefaultMacrosorCodableDefaultClient. - Build first — Run Product → Clean Build Folder, then ⌘B. Macro packages must compile the plugin on the Mac host before client code indexes correctly.
- Resolve packages — File → Packages → Reset Package Caches, then Resolve Package Versions.
- Local path — The dependency must point at the directory containing
Package.swift. - Toolchain — CodableDefault requires Swift 6.2+ (Xcode 16.3+ or a Swift 6.2 toolchain). Older Xcode versions cannot build the package.
- Check the Report navigator — If the package failed to build (e.g. macro /
swift-syntaxerrors), Xcode often still showsNo such moduleinstead of the real error.
import CodableDefault
@CodableDefault
struct Settings: Codable {
var name: String
@Default(false)
var isEnabled: Bool
@Default("guest", codingKey: "user_name")
var username: String
}
let json = #"{"name":"App"}"#.data(using: .utf8)!
let settings = try JSONDecoder().decode(Settings.self, from: json)
// settings.name == "App"
// settings.isEnabled == false
// settings.username == "guest"Role: Attached to a struct or class that conforms to Codable (or Decodable).
Generates:
enum CodingKeys— unless you already define one (see CustomCodingKeys)init(from decoder: Decoder) throws—requiredfor classes
Usage:
@CodableDefault
struct Model: Codable { ... }Role: Peer macro on a stored property. Marks a default value used when the key is missing or the value decodes as null (via decodeIfPresent).
@Default(false)
var isEnabled: Bool
@Default(10)
var retryCount: Int
@Default("guest")
var username: StringThe default expression is copied into generated code as-is (literals, .empty, [], etc.).
Role: Same as @Default, plus a custom JSON key (string raw value for CodingKeys).
@Default(false, codingKey: "is_enabled")
var isEnabled: Bool
@Default("guest", codingKey: "user_name")
var username: String| Annotation | JSON key | When key absent | When value is null |
|---|---|---|---|
| (none) | Property name | Throws | Throws (for non-optional types) |
@Default(value) |
Property name | Uses value |
Uses value |
@Default(value, codingKey: "key") |
"key" |
Uses value |
Uses value |
Required properties use:
self.name = try container.decode(String.self, forKey: .name)Defaulted properties use:
self.isEnabled =
(try? container.decodeIfPresent(Bool.self, forKey: .isEnabled))
?? falseWhen the key is present but the value has the wrong type, decoding fails and the default is used (same as missing/null). If you need strict type checking on present keys, do not use @Default for that property.
Define your own enum when you need full control (e.g. several required fields with snake_case keys). The macro does not emit CodingKeys in that case; it only emits init(from:).
Rules:
- Enum must be named
CodingKeysand conform toString, CodingKey. - Every stored instance property on the type needs a matching case name (same spelling as the property).
- Use
case propertyName = "json_key"for custom wire names.
@CodableDefault
struct Settings: Codable {
enum CodingKeys: String, CodingKey {
case name = "display_name"
case isEnabled = "is_enabled"
}
var name: String
@Default(false)
var isEnabled: Bool
}If a property has no matching case, expansion fails with a clear compile-time error.
Combine with @Default(_:codingKey:) only when the macro generates CodingKeys; if you provide the enum, put raw values on the enum cases instead.
Input:
@CodableDefault
struct Config: Codable {
var apiVersion: String
@Default(true)
var enabled: Bool
@Default(10, codingKey: "retry")
var retryCount: Int
}Expanded members (conceptually):
enum CodingKeys: String, CodingKey {
case apiVersion
case enabled
case retryCount = "retry"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.apiVersion = try container.decode(String.self, forKey: .apiVersion)
self.enabled =
(try? container.decodeIfPresent(Bool.self, forKey: .enabled))
?? true
self.retryCount =
(try? container.decodeIfPresent(Int.self, forKey: .retryCount))
?? 10
}The macros customize decoding only (init(from:)). For struct types that declare Codable, Swift can still synthesize encode(to:) as long as you do not implement it yourself. Generated CodingKeys are used for both directions when synthesis applies.
Run swift run CodableDefaultClient for a round-trip encode/decode sample.
CodableDefault/
├── Package.swift # Swift 6.2 package manifest
├── Package.resolved # Locked dependency versions
├── README.md
├── Sources/
│ ├── CodableDefault/ # Public macro declarations
│ │ └── CodableDefault.swift
│ ├── CodableDefaultMacros/ # Macro implementations (compiler plugin)
│ │ └── CodableDefaultMacro.swift
│ └── CodableDefaultClient/ # Example executable
│ └── main.swift
└── Tests/
└── CodableDefaultTests/ # End-to-end decode tests
| Target | Kind | Purpose |
|---|---|---|
CodableDefault |
Library | @CodableDefault, @Default API |
CodableDefaultMacros |
Macro / plugin | SwiftSyntax expansion |
CodableDefaultClient |
Executable | Usage demo |
CodableDefaultTests |
Tests | Runtime decode/encode tests |
CodableDefaultMacroTests |
Tests | Macro expansion tests |
swift buildswift testMacro implementations compile for the host (macOS). In Xcode, run tests with destination My Mac and scheme CodableDefault-Package. Testing against the iOS Simulator can fail because Xcode may try to build swift-syntax macro support for iOS.
swift run CodableDefaultClient- Stored properties only — must have an explicit type annotation (
var count: Int). - Static properties are ignored.
- Computed properties are ignored.
- Enums, actors, protocols are not supported as
@CodableDefaulttargets (onlystructandclass). - No custom
encode(to:)generation — decoding-only customization. - Default expressions are pasted literally; they must be valid at the use site (e.g. capture surrounding generics correctly).
- User
CodingKeysmust list every decodable stored property; partial enums are not merged automatically. - Wrong JSON types on
@Defaultfields fall back to the default value instead of throwing.
See CONTRIBUTING.md. Release history is in CHANGELOG.md.
CodableDefault is released under the MIT License.