Swift Metaprogramming: How to Write Code That Inspects Itself

By

Introduction

Most Swift developers never look beyond the syntax, crafting elegant type-safe code that compiles to efficient binaries. But what if your code could inspect itself at runtime? This capability—known as metaprogramming—lets you build generic inspectors, serializers, and dynamic APIs that adapt to the structure of your data. In this article, we dive into three powerful Swift features: Mirror, reflection, and @dynamicMemberLookup. These tools, drawn from advanced Swift internals, enable you to write code that introspects its own types and creates clean, chainable interfaces over dynamic data.

Swift Metaprogramming: How to Write Code That Inspects Itself

Understanding Reflection with Mirror

At the heart of Swift’s reflection capabilities lies the Mirror type. Introduced in Swift 2, Mirror provides a way to examine the structure of any value or object at runtime—without knowing its type at compile time. Think of it as a runtime lens that lets you iterate over properties, inspect labels, and extract values.

How Mirror Works

When you create a Mirror(reflecting: instance), Swift walks through the instance’s type metadata. The resulting mirror exposes a children collection, where each child is a tuple of an optional String label and a Any value. For classes and structs, children correspond to stored properties; for enums, they may represent associated values.

Consider this example:

struct Person {
    let name: String
    let age: Int
}

let person = Person(name: "Alice", age: 30)
let mirror = Mirror(reflecting: person)
for child in mirror.children {
    print("\(child.label ?? "unlabeled"): \(child.value)")
}
// Output:
// name: Alice
// age: 30

This simple loop works on any type, making Mirror a cornerstone for generic programming—you can write one function that inspects any struct or class without resorting to Any casting.

Practical Uses for Mirror

Mirror shines in scenarios where you need to serialize, debug, or transform objects generically. Common applications include:

  • Automatic JSON serialization: Iterate over children and map property names to JSON keys.
  • Debugging tools: Print the entire state of an object dynamically, useful for logging or crash reports.
  • Data validation: Check that all properties meet certain criteria without hardcoding each property.

However, Mirror operates on instances, not types—it cannot list static properties or methods. For that, you’d need deeper runtime access, which Swift deliberately limits for performance and safety.

Leveraging @dynamicMemberLookup

While Mirror gives you introspection, @dynamicMemberLookup brings dynamism to property access. This attribute, introduced in Swift 4.2, allows a type to define a subscript that handles arbitrary property names. It’s ideal for wrapping dictionaries, JSON objects, or other dynamic data stores in a type-safe yet flexible interface.

Building Chainable APIs

With @dynamicMemberLookup, you can create APIs that mimic natural property access on dynamic data. For example, consider a JSON wrapper:

@dynamicMemberLookup
struct JSON {
    private var data: [String: Any]

    init(_ data: [String: Any]) {
        self.data = data
    }

    subscript(dynamicMember key: String) -> JSON? {
        guard let value = data[key] else { return nil }
        if let nestedDict = value as? [String: Any] {
            return JSON(nestedDict)
        }
        return nil // simplified
    }
}

let json = JSON(["user": ["name": "Bob"]])
let name = json.user?.name  // returns JSON?

Here, json.user?.name works as if user and name were actual properties. The compiler synthesizes the subscript call, making the code readable and chainable.

Safe Dynamic Member Access

To avoid crashing on missing keys, your subscript should return an optional or use a default value. You can also combine @dynamicMemberLookup with key paths for type safety. For instance:

subscript(dynamicMember key: String) -> T? {
    return data[key] as? T
}

This allows only keys that exist at runtime—though the caller must know the expected type. For fully typed access, consider using Codable for static structs and @dynamicMemberLookup for dynamic parts.

Combining Tools for Generic Inspectors

The real power emerges when you combine Mirror and @dynamicMemberLookup. Imagine building a generic inspector that prints any object’s properties in a nicely formatted way, then allows you to drill into nested objects using dot notation. For example:

struct DeepInspector {
    let subject: Any

    func dump() {
        let mirror = Mirror(reflecting: subject)
        for child in mirror.children {
            print("\(child.label ?? "?"): \(child.value)")
            if let nestedMirror = Mirror(reflecting: child.value).children.first {
                // recursively inspect nested objects
            }
        }
    }
}

This pattern is used in libraries like SwiftGen and Sourcery (though those often rely on compile-time code generation). By using runtime reflection, you can build tools that work on any type without generating extra source code.

Conclusion

Metaprogramming in Swift opens doors to writing flexible, self-aware code. Mirror gives you runtime introspection over any instance—perfect for serialization and debugging. @dynamicMemberLookup lets you craft expressive, chainable APIs over dynamic data. Combined, they empower you to build generic inspectors that adapt to the shape of your data without sacrificing Swift’s type safety. While Swift emphasizes compile-time safety, these tools provide a controlled escape hatch for those moments when you need your code to inspect—and respond to—its own structure.

Tags:

Related Articles

Recommended

Discover More

Building Your Own J.A.R.V.I.S.-Like Smart Home: A Practical GuideCritical Bug in Linux Congestion Control Could Stall QUIC Connections, Cloudflare FindsThe Data Dilemma: Why High-Quality Human Annotation Remains AI's Greatest Bottleneck10 Ways Dart and Flutter Are Shaping AI Development in 2026Breaking: Historians Confirm 'Onna-Bugeisha' – Female Samurai Were Real Warriors in Feudal Japan