Compile-time Codable macros for resilient Swift models.
- One-line
@Codable,@Encodable, and@Decodablesynthesis - Default-aware decoding, nested keys, and graceful fallbacks
- Raw-string transcoding, lossy collections, and transformer pipelines
- Derived properties computed from already-decoded fields via
@DerivedKey - Explicit lifecycle hooks with deterministic generated code
CodableKit is a Swift macro package built for the JSON you actually receive: nested payloads, string-encoded objects, partially invalid arrays, and schemas that drift over time. It keeps configuration close to each property, surfaces mistakes with compile-time diagnostics, and avoids runtime reflection or hidden magic.
Quick Start · Feature Highlights · Targets · Installation · Migration Guide · Roadmap
import CodableKit
@Codable
struct User {
@CodableKey("data.uid")
let id: Int
var name: String
var age: Int = 24
@CodableHook(.didDecode)
mutating func normalize() {
name = name.trimmingCharacters(in: .whitespacesAndNewlines)
}
}That single model gets generated CodingKeys, init(from:), and encode(to:) implementations with nested key support, default fallback behavior, and an explicit post-decode hook.
| Capability | Example | What you get |
|---|---|---|
| Generated conformance | @Codable, @Encodable, @Decodable |
Compile-time synthesis with predictable output |
| Default values | var retries: Int = 3 |
Missing keys can fall back without hand-written decode code |
| Nested coding keys | @CodableKey("profile.info.name") |
Deep key-path mapping without manual containers |
| Graceful failure | @CodableKey(options: .useDefaultOnFailure) |
Recover from bad payloads by falling back to defaults or nil |
| Raw-string transcoding | @CodableKey(options: .safeTranscodeRawString) |
Decode string-encoded JSON into strongly typed models |
| Lossy collections | @CodableKey(options: .lossy) |
Drop invalid array, set, or dictionary entries during decode |
| Explicit hooks | @CodableHook(.didDecode) |
Run validation, normalization, or derived-value logic at clear lifecycle stages |
| Transformer pipelines | @CodableKey(transformer: MyTransformer()) |
Compose reusable decode and encode transformations; DictionaryLookupTransformer pulls a value out of a decoded dictionary, liftOptional() lifts a pipeline to optional input/output, and onFailure(_:) observes pipeline errors for logging |
| Derived properties | @Codable(derivedFrom: "slots") + @DerivedKey(transformer: MyTransformer()) |
Compute typed properties from an already-decoded sibling at the end of init(from:) — no coding key, never encoded |
| Target | Purpose |
|---|---|
CodableKit |
Public facade that exports macros, hooks, transformers, lossy wrappers, and compatibility shims |
CodableKitCore |
Canonical shared option definitions consumed by both runtime and macro targets |
CodableKitMacros |
SwiftSyntax-based code generation, diagnostics, and compiler plugin entry points |
Add CodableKit to your Swift Package Manager dependencies:
.package(url: "https://github.com/WendellXY/CodableKit.git", from: "2.0.0")Then import it where you declare your models:
import CodableKit- Swift tools 6.0
- Xcode 16+ or a Swift 6.0-compatible Apple toolchain
swift-syntax600.x- macOS 10.15+, iOS 13+, tvOS 13+, watchOS 6+, Mac Catalyst 13+, visionOS 1+
If you need the legacy Swift 5 line that targets swift-syntax 510.x, use from: "0.4.0" instead.
@Codable
struct Session {
@CodableKey("meta.version")
var version: Int = 1
@CodableKey("user.profile.name")
let name: String
}@Codable
struct Feed {
@CodableKey(options: [.lossy, .safeTranscodeRawString])
var items: [Item] = []
}@Codable(derivedFrom: "userConfigValue")
struct UserConfigInfo {
var userConfigValue: [String: String]?
@DerivedKey(
transformer: DictionaryLookupTransformer(key: "avatar_frame")
.chained(RawStringDecodingTransformer<AvatarFrame>().liftOptional())
)
private(set) var avatarFrame: AvatarFrame?
}A derived property has no CodingKeys case and is never encoded. Its value is computed at the
end of init(from:) by feeding the already-decoded source property through the transformer
pipeline, before any @CodableHook(.didDecode) hook runs. The from: source must itself be a
decoded stored property of the same type — .ignored properties are rejected at compile time.
Optional or defaulted derived properties fall back to nil/the default when the pipeline fails;
non-optional ones without a default rethrow from init(from:). Add .onFailure(_:) to the
pipeline to log failures that the fallback path would otherwise swallow.
Transformer pipelines can use .map(_:) for lightweight projections or scalar conversions.
For optional source values that should default before mapping, use .map(defaultValue:_:). For
failable mappings that also need a default output, use .map(defaultValue:fallbackValue:_:):
@DerivedKey(
transformer: DictionaryLookupTransformer(key: "account_type")
.map(defaultValue: 0, fallbackValue: AccountType.unknown, AccountType.init(rawValue:))
)
private(set) var accountType: AccountType = .unknownUse @Codable(derivedFrom:) or @Decodable(derivedFrom:) when several derived properties read
from the same decoded source. A property-level from: overrides the type-level default:
@Codable(derivedFrom: "userConfigValue")
struct UserConfigInfo {
var userConfigValue: [String: String]?
var fallbackConfigValue: [String: String]?
@DerivedKey(transformer: AvatarFrameTransformer())
private(set) var avatarFrame: AvatarFrame?
@DerivedKey(from: "fallbackConfigValue", transformer: BadgeTransformer())
private(set) var badge: Badge?
}If you mutate a source property after decoding, call rederiveValues() to refresh the derived
properties. The generated method is mutating for structs and nonmutating for classes; it is
throws when a non-optional derived property without a default can propagate transformer failures.
@Codable
struct Payload {
var value: JSONValue
}
let payload = try JSONDecoder().decode(
Payload.self,
from: #"{"value":{"name":"Ada","flags":[true,null]}}"#.data(using: .utf8)!
)
let name = payload.value["name"]?.stringValue
let firstFlag = payload.value["flags"]?[0]?.boolValue
let sameValue = try JSONValue(jsonString: #"{"name":"Ada","flags":[true,null]}"#)
let nestedName = sameValue[path: ["name"]]?.stringValue@Encodable
struct AuditEvent {
var createdAt: Date
@CodableHook(.willEncode)
func validate() throws {
// validate or normalize before encoding
}
}In v2, hooks are explicit. Conventional method names alone are not invoked anymore. See MIGRATION.md for the upgrade path.
swift build -v
swift test -vTests cover macro expansion, diagnostics, hooks, inheritance, lossy coding, nested keys, derived properties, and transformer behavior.
- Migration Guide: breaking changes and upgrade notes for v2
- Roadmap: current priorities and planned work
- Swift Package Index: compatibility, metadata, and package discovery
- Tests: real examples for structs, classes, enums, hooks, diagnostics, and transformers