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.

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

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

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

11. Practice Exercise

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.