Swift macros that make Codable decoding tolerant of missing or null JSON fields by applying compile-time default values, while leaving required properties strict. Use @Default(_:transform:) to clamp, normalize, or validate resolved values after decode. 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 — including optional post-decode transforms when you need to shape or validate the final value.
| Component | Version |
|---|---|
| Swift | 6.2+ |
| Xcode | 16+ (recommended) |
| iOS | 13+ |
| macOS | 10.15+ (required to build and run macro tooling) |
| watchOS | 6+ |
| tvOS | 13+ |
| visionOS | 1+ |
Dependencies are pinned via Package.resolved (swift-syntax 603.x, up to next minor).
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/tomisacat/CodableDefault.git", from: "2.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
2.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
@Default(10, codingKey: "retry", transform: { min($0, 100) })
var retryCount: Int
}
let json = #"{"name":"App","retry":150}"#.data(using: .utf8)!
let settings = try JSONDecoder().decode(Settings.self, from: json)
// settings.name == "App"
// settings.isEnabled == false (default — key omitted)
// settings.username == "guest" (default — key omitted)
// settings.retryCount == 100 (transform clamped 150 → 100)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: StringRole: Same as @Default, plus an optional post-decode (T) throws -> T transform applied to the resolved value — whether that value came from JSON or from the default fallback.
@Default(10, transform: { min($0, 100) })
var retryCount: Int
@Default("guest", transform: { $0.trimmingCharacters(in: .whitespaces) })
var username: String
@Default(0, codingKey: "limit", transform: { min($0, 100) })
var limit: IntThrowing transforms propagate out of init(from:). Use this for validation, clamping, or normalization after the default-or-decode step.
Defaulted properties with a transform expand to:
self.retryCount = {
let __codableDefault_retryCount =
(try? container.decodeIfPresent(Int.self, forKey: .retryCount))
?? 10
return { min($0, 100) }(__codableDefault_retryCount)
}()Non-throwing transforms omit try; throwing transforms wrap the assignment in try { … }().
| 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 |
@Default(value, transform: { … }) |
Property name | Uses transform(value) |
Uses transform(value) |
@Default(value, codingKey: "key", transform: { … }) |
"key" |
Uses transform(value) |
Uses transform(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.
Examples/CodableDefaultDemo is a SwiftUI app that walks through every major macro feature with sample JSON and live decode results.
cd Examples/CodableDefaultDemo
open CodableDefaultDemo.xcodeprojBuild and run on an iOS Simulator (iPhone 17 or later). The app groups 14 interactive scenarios covering defaults, null, required fields, custom keys, user CodingKeys, classes, transforms, validation errors, and encode round-trips.
CodableDefault/
├── Package.swift # Swift 6.2 package manifest
├── Package.resolved # Locked dependency versions
├── README.md
├── Examples/
│ └── CodableDefaultDemo/ # SwiftUI iOS demo app (local package dependency)
├── 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 (Swift Testing) |
CodableDefaultMacroTests |
Tests | Macro expansion tests (Swift Testing) |
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 CodableDefaultClientcd Examples/CodableDefaultDemo
xcodebuild build \
-project CodableDefaultDemo.xcodeproj \
-scheme CodableDefaultDemo \
-destination 'platform=iOS Simulator,name=iPhone 17'- 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 notes are in docs/releases/.
CodableDefault is released under the MIT License.