iosswiftswiftuiswiftdata

SwiftData crash: "Passed nil for a non-optional keypath" when using non-optional struct with optional properties in @Model


I’ve run into a reproducible issue when using SwiftData with @Model classes that contain non-optional value type properties (struct) with optional properties inside.

After creating a new model, the app crashes with:

Fatal error: Passed nil for a non-optional keypath \MyModel.myStruct

This happens immediately after try? modelContext.save() is called.

This is my example:

import SwiftData
import SwiftUI

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \MyModel.name) private var models: [MyModel]
    @State private var selectedModel: MyModel?

    var body: some View {
        NavigationStack {
            List(models) { model in
                Button(model.name) {
                    selectedModel = model
                }
            }
            .navigationTitle("Models")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Add") {
                        let newModel = MyModel(name: "Test")
                        modelContext.insert(newModel)
                        try? modelContext.save()
                        selectedModel = newModel
                    }
                }
            }
            .sheet(item: $selectedModel) { model in
                Text("Editing \(model.name)")
            }
        }
    }
}

@Model
class MyModel {
    var name: String
    var myStruct: MyStruct

    init(name: String) {
        self.name = name
        self.myStruct = MyStruct(value: nil)
    }
}

struct MyStruct: Codable {
    var value: String?
}

@main
struct MinimalReproApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: MyModel.self)
    }
}

If I change the value property in MyStruct so it's no longer an optional, the crash no longer occurs.

I would prefer a solution that ideally does not involve modifying MyStruct.


Solution

  • You’re encountering this crash due to how SwiftData internally manages persistence and serialization of model properties—especially with non-optional value types (struct) stored as non-optional properties in a @Model class, which themselves contain optional values.

    When you declare:
    var myStruct: MyStruct

    SwiftData treats myStruct as a non-optional embedded value, meaning it expects every key path within myStruct (i.e., value) to also be non-optional, or at the very least, to not be nil at encoding time.

    However, you’re passing:
    self.myStruct = MyStruct(value: nil)

    Even though myStruct is non-optional, its property value is nil, which causes SwiftData’s encoder to crash when it encounters a nil in a non-optional path (as far as the serialization logic is concerned). This is a known current limitation or bug in SwiftData as of iOS 17.

    The error you're seeing:
    Fatal error: Passed nil for a non-optional keypath \MyModel.myStruct
    …indicates that SwiftData’s encoder is incorrectly assuming that because myStruct is non-optional, all of its fields are also non-optional, and it cannot handle nil in a nested optional during encoding.

    You have a couple options:

    First, you can make myStruct optional — this doesn't change the implementation of MyStruct as you requested:

    @Model
    class MyModel {
        var name: String
        var myStruct: MyStruct?
    
        init(name: String) {
            self.name = name
            self.myStruct = MyStruct(value: nil)
        }
    }
    

    This is currently the only reliable way to use structs with optional members in SwiftData.

    Alternatively, you could provide a non-optional value at init-time (if you really want myStruct non-optional)

    init(name: String) {
        self.name = name
        self.myStruct = MyStruct(value: "")
    }
    

    But this changes the semantics of your data—"" is not the same as nil.

    Hope that helps, or gets you closer to the answer you're looking for!