# Swift Metaprogramming: Reflection, Macros, and Code Generation

## Pro Summary

Swift has two metaprogramming layers. `Mirror` is read-only runtime reflection over stored properties: it can't call methods, mutate values, or show computed properties, and it reports whatever storage the compiler generated or a `CustomReflectable` conformance chooses to expose. Macros (Swift 5.9+) are compile-time syntax transforms that run as sandboxed SwiftSyntax compiler plugins. They see only syntax, never type information, and an attached macro must declare in its role every name it introduces. Before reaching for either one, check whether compiler synthesis (`Equatable`, `Hashable`, `Codable`), key paths, or generics already solve the problem.

```
// Runtime reflection (read-only)
for child in Mirror(reflecting: value).children { print(child.label ?? "_", child.value) }

// Macro declarations; the implementations live in a separate .macro target
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacros", type: "StringifyMacro")

@attached(extension, conformances: Hashable, names: named(hash(into:)))
public macro AutoHashable() = #externalMacro(module: "MyMacros", type: "AutoHashableMacro")
```

Requires: `Mirror`: any Swift version. Macros: Swift 5.9+ (Xcode 15+), implemented in a SwiftPM `.macro` target that depends on swift-syntax.

Gotchas:

- `Mirror` lists only the type's own stored properties. Superclass storage is under `superclassMirror`, and computed properties never appear.
- `Mirror` shows generated storage, not your API: an `@Observable` class reflects as `_count`, `_name`, `_$observationRegistrar`, a property wrapper as `_level`, and `lazy var v` as `$__lazy_storage_$_v`. Mirror-based loggers and serializers break when a model adopts these features.
- Any type can replace its reflection with `CustomReflectable`, which is handy for redacting secrets but means `Mirror` output isn't a reliable view of real state.
- Macro plugins run sandboxed with no file-system or network access and see only the syntax they're attached to. Generating code from a schema or resource file is a job for a SwiftPM build-tool plugin.
- An attached macro can only introduce names covered by its `names:` list (`named(x)`, `prefixed(_)`, `arbitrary`, and so on) or created with `context.makeUniqueName(_:)`. Peer macros on top-level declarations can't use `arbitrary`.

Primary sources:

- [SE-0382: Expression Macros](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0382-expression-macros.md)
- [SE-0389: Attached Macros](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0389-attached-macros.md)
- [SE-0402: Generalize conformance macros as extension macros](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0402-extension-macros.md)
- [Apple Developer Documentation: Mirror](https://developer.apple.com/documentation/swift/mirror)

Source: https://devdocs10.com/articles/swift/swift-metaprogramming  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

Swift metaprogramming means writing code that can inspect, describe, or generate other code or code-like behavior. In Swift, this usually includes reflection with `Mirror`, compile-time macros, and patterns that reduce boilerplate by letting the compiler help write repetitive code for you.

> **Quick answer:** Swift metaprogramming is not one single feature. For runtime inspection use `Mirror`; for compile-time code generation use macros; and for reusable boilerplate reduction use protocol-oriented design plus tooling.

> **Difficulty:** Advanced
>
> **You'll understand this better if you know:** Swift types, protocols, generics, and how functions and structs are written.

## 1. What Is Swift Metaprogramming?

Metaprogramming is programming that works with programs, types, or source-like structure instead of only with raw data. In Swift, the term usually covers a few related ideas:

- Inspecting values and types at runtime with `Mirror`.
- Generating source-level code at compile time with macros.
- Using generic and protocol-based abstractions to remove repetition.
- Building libraries that can adapt behavior based on type information.

Swift does not have the same style of unrestricted runtime metaprogramming you may see in some dynamic languages. That is a design choice: Swift prefers safety, predictable compilation, and strong type checking.

## 2. Why Swift Metaprogramming Matters

Metaprogramming matters because many Swift projects contain repeated patterns: encoding models, formatting output, deriving debug strings, validating data, and generating helper APIs. Without metaprogramming, those tasks can turn into large amounts of manual code.

Swift gives you a controlled set of tools that help in three useful ways:

- Reduce boilerplate for common patterns.
- Keep logic synchronized with your types and properties.
- Shift work from runtime to compile time when possible.

This is especially valuable in large codebases where consistency matters more than cleverness. Used well, metaprogramming improves maintainability. Used poorly, it can make code harder to understand or debug.

## 3. Basic Syntax

Swift metaprogramming is not a single syntax feature, so the core idea depends on the technique you use. A good starting point is reflection with `Mirror`, which lets you inspect a value’s children at runtime.

### Inspecting a value with Mirror

The following example prints the stored properties of a simple struct:

```swift
struct User {
    let name: String
    let age: Int
}

let user = User(name: "Ava", age: 28)
let mirror = Mirror(reflecting: user)

for child in mirror.children {
    if let label = child.label {
        print(label, child.value)
    }
}
```

Output (tested with Swift 6.4):

```text
name Ava
age 28
```

This works because `Mirror` exposes the reflected value’s children, such as property names and values, without needing to hardcode the structure.

### Compile-time code generation with macros

Macros are the modern Swift feature for generating code at compile time. They help you write less repetitive code while keeping the result visible to the compiler.

```swift
import Observation

// Conceptual example: a macro can synthesize code from a declaration.
@Observable
final class Store {
    var count = 0
}
```

In this style, the macro expands into code the compiler understands, rather than making changes at runtime. The exact expansion depends on the macro definition.

## 4. Step-by-Step Examples

Swift metaprogramming is easiest to understand by comparing its major use cases. The examples below show practical patterns you can build with today.

### Example 1: Debugging an unknown value

Reflection is useful when you have a value and do not want to manually write one-off inspection code.

```swift
struct Point {
    let x: Int
    let y: Int
}

let point = Point(x: 3, y: 5)
let mirror = Mirror(reflecting: point)

print(mirror.displayStyle ?? "unknown")
```

Output (tested with Swift 6.4):

```text
struct
```

This tells you the reflected value is a struct and gives you a lightweight way to inspect it.

### Example 2: Building a generic string representation

You can use `Mirror` to build a debugging helper for many types.

```swift
func debugSummary(of value: Any) -> String {
    let mirror = Mirror(reflecting: value)
    let typeName = String(describing: type(of: value))
    let pairs = mirror.children.compactMap { child -> String? in
        guard let label = child.label else { return nil }
        return "\(label): \(child.value)"
    }

    return "\(typeName)(\(pairs.joined(separator: ", ")))"
}
```

This is practical for logs, debug tools, and quick inspection utilities.

### Example 3: Synthesized conformances vs. macro-generated boilerplate

Macros shine when you want the compiler to synthesize a pattern repeatedly. In modern Swift, this often replaces older manual boilerplate.

```swift
// Built in: declaring the conformance is enough. No macro is needed.
struct Profile: Equatable, Hashable, Codable {
    let id: Int
    let name: String
}

// Illustrative: a hypothetical macro for boilerplate Swift does not synthesize,
// such as a public memberwise initializer.
@PublicMemberwiseInit
public struct Account {
    public let id: Int
    public let owner: String
}
```

The compiler already synthesizes `Equatable`, `Hashable`, and `Codable` when every stored property conforms, so those conformances never need a macro. Macros are worth it for boilerplate the compiler does not generate, such as a public memberwise initializer (Swift only synthesizes an internal one).

### Example 4: Comparing reflection and manual code

For predictable domain logic, manual code is often clearer than reflection.

```swift
struct Invoice {
    let number: String
    let total: Double

    var summary: String {
        "Invoice \(number): $\(total)"
    }
}
```

Even though you could build this by reflection, the explicit property is easier to read and safer to maintain.

## 5. Practical Use Cases

Swift metaprogramming is most useful when repetition is high and the structure is stable. Common use cases include:

- Debug printing for models, payloads, or test fixtures.
- Generating equality, hashing, and formatting code from stored properties.
- Creating DSL-like APIs where syntax should stay concise.
- Inspecting property metadata in developer tools.
- Reducing boilerplate in serialization, validation, and view models.

As a rule, use metaprogramming when it improves correctness or eliminates repeated code that would otherwise drift apart.

## 6. Common Mistakes

### Mistake 1: Using Mirror for business logic

Reflection is tempting because it can inspect many values automatically, but it is usually the wrong choice for core app behavior. Business rules should be explicit, because reflection-based code is harder to reason about and can break when stored properties change.

**Problem:** This code assumes every type has a property called `status`, but reflection does not guarantee that.

```swift
func isActive(_ value: Any) -> Bool {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        if child.label == "status" {
            return "active" == String(describing: child.value)
        }
    }
    return false
}
```

**Fix:** Model the behavior with a protocol or explicit property instead of searching through reflected storage.

```swift
protocol Activatable {
    var isActive: Bool { get }
}

struct Account: Activatable {
    let isActive: Bool
}
```

The corrected version works because the compiler can enforce the contract directly.

### Mistake 2: Expecting Mirror to expose everything

`Mirror` only shows reflected children, and it does not behave like full source introspection. Many beginners expect methods, computed properties, and all implementation details to appear automatically.

**Problem:** This code expects a computed property to appear in `Mirror`, but only stored properties are reflected as children in the way most developers expect.

```swift
struct Temperature {
    let celsius: Double
    var fahrenheit: Double {
        celsius * 1.8 + 32
    }
}

let mirror = Mirror(reflecting: Temperature(celsius: 20))
print(mirror.children)
```

Output (tested with Swift 6.4):

```text
AnyCollection<(label: Optional<String>, value: Any)>(_box: Swift._RandomAccessCollectionBox<Swift.LazyMapSequence<Swift.Range<Swift.Int>, (label: Swift.Optional<Swift.String>, value: Any)>>)
```

**Fix:** Use explicit APIs when you need reliable access to derived values.

```swift
struct Temperature {
    let celsius: Double
    var fahrenheit: Double {
        celsius * 1.8 + 32
    }
}

let reading = Temperature(celsius: 20)
print(reading.fahrenheit)
```

Output (tested with Swift 6.4):

```text
68.0
```

The corrected version works because the property is accessed through normal Swift behavior instead of a partial reflective view.

### Mistake 3: Using macros where plain Swift is clearer

Macros can reduce boilerplate, but they also add indirection. If a task is small and stable, a macro may make maintenance harder rather than easier.

**Problem:** This approach hides simple logic behind generated code even though the behavior is easy to write directly.

```swift
// Overusing a macro for a tiny one-off formatting task.
@GenerateDisplayName
struct Tag {
    let value: String
}
```

**Fix:** Write the small function directly unless the pattern repeats many times.

```swift
struct Tag {
    let value: String

    var displayName: String {
        value.uppercased()
    }
}
```

The corrected version works because it is easier to read, test, and understand at a glance.

## 7. Best Practices

### Practice 1: Prefer compile-time generation when possible

When a repeated pattern can be generated before the program runs, prefer compile-time solutions: built-in conformance synthesis first, then macros for patterns the compiler does not cover. They make behavior easier for the compiler to validate and usually produce better diagnostics than runtime reflection.

```swift
// Built in: Codable is synthesized at compile time, with no macro or reflection.
struct Address: Codable {
    let street: String
    let city: String
}
```

This keeps the source compact while preserving compile-time checks.

### Practice 2: Use reflection for inspection, not decisions

Reflection is excellent for logging, tooling, or exploratory code. It is usually a poor fit for application logic that should remain stable over time.

```swift
func logChildren(of value: Any) {
    let mirror = Mirror(reflecting: value)
    for child in mirror.children {
        print(child.label ?? "-", child.value)
    }
}
```

This works well because diagnostics are exactly the kind of task where approximate metadata is useful.

### Practice 3: Keep macro expansion understandable

If you use macros, make sure the generated result is predictable. Prefer simple expansions that map clearly to the original source.

```swift
// A macro that synthesizes a small, obvious helper is easier to maintain
// than one that generates many hidden behaviors.
@AddDescription
struct Book {
    let title: String
}
```

When developers can guess what the expansion does, they are more likely to trust and reuse it.

## 8. Limitations and Edge Cases

- `Mirror` is not a full source parser; it reflects values, not all code structure.
- Performance-sensitive code should avoid heavy runtime reflection in tight loops.
- Macros run at compile time, so they cannot inspect live runtime data.
- Generated code can be harder to debug if the expansion is complex or poorly documented.
- Reflection behavior can differ across value types, reference types, and collections.
- Many forms of metaprogramming are less portable than plain Swift because they rely on compiler support or specific library design.

> A common `not working` scenario is expecting a macro to react to data loaded from disk or the network. Macros cannot do that because the data does not exist at compile time.

## 9. Practical Mini Project

Here is a small utility that uses reflection to print a readable summary for any value. It is simple enough to understand but still realistic for logging and diagnostics.

```swift
struct Order {
    let id: Int
    let customer: String
    let total: Double
}

func describe(_ value: Any) -> String {
    let mirror = Mirror(reflecting: value)
    let typeName = String(describing: type(of: value))
    let parts = mirror.children.compactMap { child -> String? in
        guard let label = child.label else { return nil }
        return "\(label)=\(child.value)"
    }

    return "\(typeName)(\(parts.joined(separator: ", ")))"
}

let order = Order(id: 101, customer: "Mina", total: 84.5)
print(describe(order))
```

Output (tested with Swift 6.4):

```text
Order(id=101, customer=Mina, total=84.5)
```

This utility demonstrates the most common metaprogramming pattern in Swift: use runtime reflection to turn structured values into useful metadata for humans.

## 10. Key Points

- Swift metaprogramming usually means reflection, macros, or boilerplate-reducing abstractions.
- `Mirror` is for inspecting values at runtime, not for replacing normal model design.
- Macros are the best option when you want compile-time generation and stronger compiler support.
- Explicit Swift code is often clearer than clever metaprogramming for small tasks.
- The safest metaprogramming is the kind that keeps the generated behavior easy to predict.

## 11. Practice Exercise

- Create a `Person` struct with three stored properties: `firstName`, `lastName`, and `age`.
- Write a function that uses `Mirror` to produce a string in the format `Person(firstName=..., lastName=..., age=...)`.
- Skip children without labels.
- Print the result for one example value.

**Expected output:** A single line describing the person and each labeled stored property.

**Hint:** Use `compactMap` to filter out unlabeled children and `joined(separator:)` to combine the pieces.

```swift
struct Person {
    let firstName: String
    let lastName: String
    let age: Int
}

func describe(_ value: Any) -> String {
    let mirror = Mirror(reflecting: value)
    let typeName = String(describing: type(of: value))

    let parts = mirror.children.compactMap { child -> String? in
        guard let label = child.label else { return nil }
        return "\(label)=\(child.value)"
    }

    return "\(typeName)(\(parts.joined(separator: ", ")))"
}

let person = Person(firstName: "Noah", lastName: "Kim", age: 31)
print(describe(person))
```

Output (tested with Swift 6.4):

```text
Person(firstName=Noah, lastName=Kim, age=31)
```

This solution works because it inspects the stored properties in a safe, reusable way while still staying within plain Swift.

## 12. Final Summary

Swift metaprogramming is about making code smarter about code, types, and structure. In practice, that usually means runtime reflection with `Mirror`, compile-time code generation with macros, and careful use of abstractions that reduce repetition without hiding intent.

The most important decision is not whether to use metaprogramming, but how much. For logs, tooling, and generated boilerplate, it can be a huge win. For business logic, explicit Swift is often easier to maintain and safer for future changes.

If you want to go further, the next best step is to study Swift macros and then compare them with protocol-oriented design so you can choose the simplest solution for each task.

## Where AI Coding Assistants Get This Wrong

- Often suggested: Implements a conformance-adding macro with the `ConformanceMacro` protocol and the `@attached(conformance)` role.
  Instead: Implement `ExtensionMacro`, declare `@attached(extension, conformances: P, names: ...)`, and return `ExtensionDeclSyntax` values. (SE-0402 generalized conformance macros into extension macros, and `ConformanceMacro` no longer exists in current swift-syntax, so code from early tutorials doesn't build.)
- Often suggested: Writes a macro that reads a JSON or OpenAPI file, calls the network, or checks a property's type (for example, 'only if it conforms to `Codable`') during expansion.
  Instead: Keep macros purely syntactic and emit code that the type checker validates, such as generic constraints. Use a SwiftPM build-tool plugin for file-driven code generation. (Macro implementations run without file-system or network access and receive syntax only, with no type information.)
- Often suggested: Builds a generic JSON encoder or a property setter on top of `Mirror`, or calls KVC `setValue(_:forKey:)` on Swift structs.
  Instead: Use synthesized `Codable` for serialization and `WritableKeyPath` for dynamic, type-safe property access. (`Mirror` is read-only and exposes generated storage names like `_count`, and KVC works only on `@objc` properties of `NSObject` subclasses.)
