Skip to content

WendellXY/CodableKit

Repository files navigation



CodableKit logo

swift versions platform support build status license

CodableKit

Compile-time Codable macros for resilient Swift models.

  • One-line @Codable, @Encodable, and @Decodable synthesis
  • 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

Quick Start

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.

Feature Highlights

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

Targets

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

Installation

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

Requirements

  • Swift tools 6.0
  • Xcode 16+ or a Swift 6.0-compatible Apple toolchain
  • swift-syntax 600.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.

Examples

Nested keys and defaults

@Codable
struct Session {
  @CodableKey("meta.version")
  var version: Int = 1

  @CodableKey("user.profile.name")
  let name: String
}

Lossy collections and raw-string payloads

@Codable
struct Feed {
  @CodableKey(options: [.lossy, .safeTranscodeRawString])
  var items: [Item] = []
}

Derived properties

@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 = .unknown

Use @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.

Dynamic JSON values

@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

Explicit lifecycle hooks

@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.

Development

swift build -v
swift test -v

Tests cover macro expansion, diagnostics, hooks, inheritance, lossy coding, nested keys, derived properties, and transformer behavior.

Docs

  • 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

License

MIT

About

A Swift macro package designed to simplify the use of Swift's Codable protocol by allowing easy integration of default values, reducing the amount of auxiliary code you need to write.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages