Swift Reflection & Mirror API: Inspecting Values at Runtime
Swift’s Mirror API lets you inspect the structure of a value at runtime. You can discover a value’s type, stored properties, child values, and some collection-like details without hard-coding every field.
Quick answer: Mirror is Swift’s standard-library reflection tool for reading type and property information at runtime. It is useful for debugging, logging, and building generic utilities, but it is read-only and should not be used as a replacement for normal type-safe code.
Difficulty: Intermediate
You'll understand this better if you know: basic Swift types, structs and classes, optionals, and how properties and collections work.
1. What Is Swift Reflection & Mirror API?
Reflection is the ability to examine a value while your program is running. In Swift, the standard way to do this is with Mirror.
- It shows the subjectType of a value.
- It exposes stored properties as child values when available.
- It can reveal labels for those children, such as property names.
- It works with many standard Swift values, including structs, classes, enums, tuples, collections, and optionals.
Think of Mirror as a read-only inspection window. It helps you ask, “What is this value made of?” without needing special support from the type itself.
2. Why Swift Reflection & Mirror API Matters
Mirror matters because some problems are easier to solve when code can inspect values generically. It is especially useful when you want to log unknown types, build debugging tools, or write utilities that work across many model types.
In everyday Swift app development, you usually prefer explicit, type-safe access. But reflection becomes useful when you do not know the concrete type in advance or when you want reusable tooling.
Use Mirror when you need runtime inspection. Avoid it when a regular property, protocol requirement, or generic function would be clearer and safer.
3. Basic Syntax or Core Idea
Create a mirror
You create a mirror by passing a value to Mirror(reflecting:).
struct User {
let name: String
let age: Int
}
let user = User(name: "Ava", age: 28)
let mirror = Mirror(reflecting: user)
print(mirror.subjectType)This prints the reflected type, which is usually the first thing you want to know. From there, you can inspect children.
Inspect children
The children property is a sequence of labeled values. Each child usually represents a stored property or element.
for child in mirror.children {
print(child.label ?? "" , child.value)
}Each child has a label and a value. Labels may be missing, especially for tuples or some internal values.
Read the display style
The displayStyle tells you what broad kind of value you are inspecting, such as a struct, class, enum, tuple, collection, dictionary, or optional.
if let style = mirror.displayStyle {
print(style)
}This is useful when you want to branch on the shape of a value without hard-coding specific types.
4. Step-by-Step Examples
Example 1: Reflecting a simple struct
Start with a small model type so you can see how stored properties appear in a mirror.
struct Book {
let title: String
let pages: Int
}
let book = Book(title: "Swift in Practice", pages: 320)
let mirror = Mirror(reflecting: book)
print(mirror.subjectType)
for child in mirror.children {
print(child.label ?? "" , child.value)
}This shows the type and each stored property. It is a good baseline for understanding how reflection presents user-defined values.
Example 2: Inspecting an enum with an associated value
Enums can also be reflected. With an associated value, Mirror reveals the active case and its payload.
enum NetworkState {
case idle
case loading(Int)
case failed(String)
}
let state = NetworkState.failed("Timeout")
let mirror = Mirror(reflecting: state)
print(mirror.displayStyle ?? "none")
for child in mirror.children {
print(child.label ?? "" , child.value)
}For enums, the reflected shape depends on the active case. This is useful in diagnostic tools, but you still usually handle enums with switch in normal code.
Example 3: Reflecting a collection
Collections are reflected too. A mirror can help you inspect array contents or dictionary entries while debugging.
let numbers = [10, 20, 30]
let mirror = Mirror(reflecting: numbers)
print(mirror.displayStyle ?? "none")
for child in mirror.children {
print(child.label ?? "" , child.value)
}Arrays usually have unlabeled children because the elements are indexed rather than named. The mirror still gives you a straightforward way to inspect the elements.
Example 4: Using descendant for nested values
If a type contains nested data, you can use descendant(_:) to fetch a child by path rather than iterating manually.
struct Address {
let city: String
let zipCode: String
}
struct Profile {
let username: String
let address: Address
}
let profile = Profile(
username: "mira",
address: Address(city: "Berlin", zipCode: "10115")
)
let mirror = Mirror(reflecting: profile)
if let city = mirror.descendant("address", "city") {
print(city)
}This is handy when you need to inspect a known nested path dynamically. If the path is wrong, the result is nil.
5. Practical Use Cases
- Building debug log output for model objects without manually writing a formatter for every type.
- Creating generic inspection tools in developer consoles or test helpers.
- Writing diagnostic code that shows nested property values when a test fails.
- Handling mixed value types in a generic library where the concrete type is not known in advance.
- Detecting whether a value behaves like a collection, optional, tuple, struct, class, or enum at runtime.
Mirror is not the right tool for domain logic, but it is very helpful for instrumentation and introspection.
6. Common Mistakes
Mistake 1: Treating Mirror as a way to modify values
Mirror is read-only. Beginners sometimes expect it to let them set properties dynamically, but Swift reflection does not provide mutation APIs for ordinary value inspection.
Problem: This code suggests that Mirror can change the reflected value, but the API only exposes metadata and child values.
struct User {
var name: String
}
var user = User(name: "Ava")
let mirror = Mirror(reflecting: user)
// There is no API here to assign a new value through Mirror.Fix: Use Mirror to inspect, then update values through normal Swift code or dedicated mutating methods.
struct User {
var name: String
mutating func rename(to newName: String) {
name = newName
}
}
var user = User(name: "Ava")
user.rename(to: "Mina")The corrected version works because reflection is used for inspection only, while mutation stays in regular Swift methods.
Mistake 2: Assuming all children have labels
Many values do have labels, but not all of them. Arrays, tuples, and some internal representations can return unlabeled children.
Problem: This code force-unwraps a label that may be nil, which can crash at runtime.
let values = [1, 2, 3]
let mirror = Mirror(reflecting: values)
for child in mirror.children {
print(child.label!)
}Fix: Handle the optional label safely and provide a fallback when it is missing.
let values = [1, 2, 3]
let mirror = Mirror(reflecting: values)
for child in mirror.children {
print(child.label ?? "element", child.value)
}This version is safer because it respects the fact that reflected children may not have names.
Mistake 3: Relying on Mirror for stable serialization
Reflection output is not a contract for storage or network formats. The order and shape of children can change across implementations, compiler versions, or type design changes.
Problem: This approach assumes reflection is a stable replacement for a real encoder, which can lead to fragile output and broken parsing.
struct Person {
let firstName: String
let lastName: String
}
let person = Person(firstName: "Sam", lastName: "Lee")
let mirror = Mirror(reflecting: person)
// Do not build file formats or API payloads by stringifying Mirror children.Fix: Use explicit models and standard encoders such as Codable when you need a stable representation.
struct Person: Codable {
let firstName: String
let lastName: String
}
let person = Person(firstName: "Sam", lastName: "Lee")
let data = try JSONEncoder().encode(person)The corrected version works because stable serialization belongs to encoding APIs, not reflection.
7. Best Practices
Use reflection for diagnostics, not core business logic
Reflection is excellent for logging and debugging, but code that depends on it for everyday behavior is harder to read and more brittle.
Prefer explicit logic:
func describe(_ user: User) -> String {
"\(user.name)"
}Use Mirror only when the type is truly unknown or when you are building generic tooling.
Check for optional metadata safely
Mirror uses optional values for labels and display style, so safe handling keeps your code robust.
let mirror = Mirror(reflecting: 42)
if let style = mirror.displayStyle {
print(style)
}
This avoids assuming that every reflected value exposes the same metadata.
Use helper functions to keep reflection output readable
Reflection output can become noisy very quickly. Wrap it in a helper so your logs are consistent and easy to scan.
func dumpChildren(of value: some Any) {
let mirror = Mirror(reflecting: value)
print("Type:", mirror.subjectType)
for child in mirror.children {
print(child.label ?? "" , "=", child.value)
}
}A helper like this keeps reflection centralized instead of scattered throughout your codebase.
8. Limitations and Edge Cases
- Mirror only inspects the public runtime shape of values; it does not expose every implementation detail.
- Stored property order is not something you should treat as a guaranteed API contract.
- Computed properties usually do not appear as children because they are not stored values.
- Private or internal design details may not be represented the way beginners expect.
- Reflection output can differ between structs, classes, tuples, enums, collections, dictionaries, and optionals.
- Custom types can supply their own reflection behavior, so the observed children may not exactly match stored properties.
- Using reflection in hot code paths can add overhead compared with direct access.
Warning: Do not use reflection output for security-sensitive decisions or persistent file formats. It is meant for inspection, not as a stable data contract.
9. Practical Mini Project
Here is a small console-style utility that prints a readable summary of any Swift value. It shows the type, display style, and each child if one exists.
struct Address {
let city: String
let zipCode: String
}
struct Person {
let name: String
let age: Int
let address: Address
}
func describeValue(_ value: some Any) {
let mirror = Mirror(reflecting: value)
print("Type:", mirror.subjectType)
if let style = mirror.displayStyle {
print("Style:", style)
}
for child in mirror.children {
print(child.label ?? "" , "=", child.value)
}
}
let person = Person(
name: "Jordan",
age: 34,
address: Address(city: "Lisbon", zipCode: "1100-000")
)
describeValue(person)This mini project is useful as a debugging aid. You can pass any value into describeValue and inspect its runtime structure without writing a custom formatter for each type.
10. Key Points
- Mirror is Swift’s built-in way to inspect values at runtime.
- It is mainly for debugging, logging, and generic tooling.
- subjectType, children, and displayStyle are the core properties to learn first.
- Children often have labels for stored properties, but not always.
- Reflection is read-only and should not replace normal Swift models or encoders.
11. Practice Exercise
- Create a struct named ServerConfig with at least three stored properties.
- Write a function that accepts any value and prints its type plus each child label and value.
- Pass a ServerConfig instance into the function and confirm the output is readable.
Expected output: A type name followed by each stored property on its own line.
Hint: Use Mirror(reflecting:) and remember that labels are optional.
struct ServerConfig {
let host: String
let port: Int
let usesTLS: Bool
}
func printMirrorSummary(_ value: some Any) {
let mirror = Mirror(reflecting: value)
print("Type:", mirror.subjectType)
for child in mirror.children {
print(child.label ?? "" , "=", child.value)
}
}
let config = ServerConfig(host: "localhost", port: 8080, usesTLS: false)
printMirrorSummary(config)12. Final Summary
Mirror gives Swift developers a simple way to inspect values at runtime. It can reveal the reflected type, child values, labels, and a broad display style, which makes it a strong fit for debugging tools and generic inspection utilities.
At the same time, reflection has real limits. It is read-only, it is not a serialization strategy, and it should not replace explicit model APIs when you already know the type. The best use of Mirror is to help you understand values you do not fully control or to build tooling that aids debugging and diagnostics.
If you want to go further, the next useful topic is Swift type erasure and protocols, because those patterns often pair well with reflection when you are designing flexible but still type-safe code.