Swift Metaprogramming: Writing Code that Inspects Itself
Most Swift developers never look beyond the syntax—but what if your code could inspect itself at runtime? This extract from Swift Internals explores Mirror, reflection, and @dynamicMemberLookup: the metaprogramming tools that let you build generic inspectors and clean, chainable APIs over dynamic data. By Aaqib Hussain.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Metaprogramming: Writing Code that Inspects Itself
15 mins
- The Magic Mirror: Runtime Reflection with Mirror
- What is Reflection?
- How to Use Mirror?
- Practical Use Case: Building a Generic prettyPrint
- The Limitations of the Mirror
- Dynamic Lookups: @dynamicMemberLookup
- What is @dynamicMemberLookup?
- Applying the @dynamicMemberLookup Attribute
- Practical Use Case: A Type-Safe JSON Wrapper
- What Comes Next?
- Get the Full Picture with Swift Internals
The Limitations of the Mirror
While Mirror is a phenomenal tool, it comes with important trade-offs, especially compared to reflection in more dynamic languages.
First and foremost, Mirror is read-only. You can inspect an object, read its property names, and get its values, but you cannot modify them. You cannot use Mirror to set new values for the age property of the User object or change its name property. Swift’s strict emphasis on type safety and immutability prevents this kind of “backdoor” access.
Second, reflection is slow. Because it occurs entirely at runtime, it involves dynamic type checking, creating new collection wrappers for children, and boxing values into Any. It also prevents many compile-time optimizations that Swift normally relies on. While it’s perfect for debugging, logging, or serialization, you should never use Mirror in performance-critical tasks. It is a heavy, dynamic tool in a language optimized for static performance.
Dynamic Lookups: @dynamicMemberLookup
@dynamicMemberLookup lets you intercept accesses to members that don’t exist at compile time.
Normally in Swift, if you write someInstance.someProperty and someProperty doesn’t exist, the compiler throws an error and stops you immediately. This is a core safety feature. But what about when you’re working with inherently dynamic data, like JSON, where the keys are unknown until runtime? @dynamicMemberLookup makes this possible by letting you create clean, dot-syntax APIs over data that is inherently unstructured.
What is @dynamicMemberLookup?
@dynamicMemberLookup is an attribute that you can apply to a struct, class, or enum. It fundamentally alters how the compiler handles property access on that type.
When you apply this attribute, you’re making a promise to the compiler. “Hey compiler, if you see someone try to access a property on this type that you don’t recognize, don’t throw an error. Instead, just trust me. At runtime, I will provide an implementation that handles that call.” This lets you adopt dynamic behavior found in languages like Python or JavaScript, but in a controlled, explicit way.
Applying the @dynamicMemberLookup Attribute
To fulfill the promise with the compiler, the type marked with @dynamicMemberLookup must implement a special subscript method: subscript(dynamicMember member: String). The String parameter represents the member name extracted from the dot syntax.
When the compiler encounters a dot-syntax access to an unresolved member, it rewrites that expression into a call to this subscript. That’s where the compiler performs this magic translation.
Check the following example of using the @dynamicMemberLookup attribute on a dynamic dictionary.
@dynamicMemberLookup
struct DynamicDictionary {
private var data: [String: Any]
init(_ data: [String: Any]) {
self.data = data
}
// The required subscript
subscript(dynamicMember member: String) -> Any? {
print("Dynamic lookup for member: '\(member)'")
return data[member]
}
}
Now you can call the properties like:
let user = DynamicDictionary(["name": "Jim Halpert", "age": 33])
// 1
let name = user.name
// 2
print(name)
A quick analysis of each line is as follows:
- The dot syntax is available, and the compiler doesn’t give you an error.
- It prints:
Dynamic lookup for member: 'name'Optional("Jim Halpert")
This compiler trick is fundamental to dynamic member lookup. It secretly converts simple dot-syntax (user.name) into string-based dictionary lookups (user[dynamicMember: "name"]), providing the best of both worlds.
Practical Use Case: A Type-Safe JSON Wrapper
The most common and powerful use case for @dynamicMemberLookup is building a wrapper that makes JSON-style access cleaner and more ergonomic.
You should be aware of the pyramid of doom. The indentation gets so deep that you practically need a climbing rope and a headlamp to find your way back out. It’s typically caused by nested casting when accessing dictionary values. The code usually looks like this:
// The "Before" - Painful, nested casting
var userName: String?
if let userDict = json["user"] as? [String: Any] {
if let nameValue = userDict["name"] as? String {
userName = nameValue
}
}
This is difficult to read and very fragile. You can improve this by creating a JSON struct that wraps your data and uses @dynamicMemberLookup for a clean, chainable approach. Take a look at the code below:
@dynamicMemberLookup
struct JSON {
private var data: Any?
init(_ data: Any?) {
self.data = data
}
subscript(dynamicMember member: String) -> JSON {
guard let dict = data as? [String: Any] else {
return JSON(nil)
}
return JSON(dict[member])
}
var string: String? {
return data as? String
}
var int: Int? {
return data as? Int
}
var array: [JSON]? {
guard let arr = data as? [Any] else { return nil }
return arr.map { JSON($0) }
}
}
Now, you can use this wrapper like this:
// The "After" - Clean, chainable, and readable
let userData: [String: Any] = [
"user": [
"name": "Michael G. Scott",
"age": 44
]
]
let json = JSON(userData)
let name = json.user.name.string
print(name) // Prints Optional("Michael G. Scott")
To understand what is happening here:
-
json.user: The compiler cannot find theuserproperty. It calls thesubscript(dynamicMember: "user"). This retrieves the user object from the provideduserDatadictionary and returns a newJSONstruct that wraps that dictionary. -
.name: This is called on the newJSONstruct. The compiler again callssubscript(dynamicMember: "name"). This finds the name property within the user object and returns a newJSONobject containing only that string. -
.string: This is a standard property call on theJSONstruct. It attempts to cast its internal data ("Michael G. Scott") to aStringand returns it.
This is a great example of metaprogramming in practice: you built a tool that enables clean, chainable, API-like syntax while dynamically managing unstructured data at runtime.
What Comes Next?
Inspecting objects during runtime is interesting; what’s even more exciting is working with powerful compile-time metaprogramming concepts. This is where the code’s structure is modified as it’s being compiled.
This extract is just the beginning of the metaprogramming chapter in Swift Internal. In the rest of this chapter, you’ll discover @resultBuilder — the engine behind SwiftUI’s declarative syntax — and learn to build your own Domain-Specific Languages directly in Swift. Then you’ll get hands-on with Swift Macros, one of the biggest shifts in Swift metaprogramming to date, which let you generate code during compilation, eliminating entire categories of boilerplate with a single annotation. If the runtime techniques you’ve seen here are about holding a mirror up to your code, what comes next is about teaching the compiler to write code for you.