SwiftData & CloudKit: Arrays of Codable Structs Causing NSKeyedUnarchiveFromData Error

I have SwiftData models containing arrays of Codable structs that worked fine before adding CloudKit capability. I believe they are the reason I started seeing errors after enabling CloudKit.

Example model:

@Model
final class ProtocolMedication {
    var times: [SchedulingTime] = []  // SchedulingTime is Codable
    // other properties...
}

After enabling CloudKit, I get this error logged to the console:

'NSKeyedUnarchiveFromData' should not be used to for un-archiving and will be removed in a future release

CloudKit Console shows this times data as "plain text" instead of "bplist" format. Other struct/enum properties display correctly (I think) as "bplist" in CloudKit Console.

The local SwiftData storage handled these arrays fine - this issue only appeared with CloudKit integration.

What's the recommended approach for storing arrays of Codable structs in SwiftData models that sync with CloudKit?

Answered by DTS Engineer in 857407022

Did you wait some time to make sure the error didn't pop up? It might take a minute or two. Try inserting a record then re-running and waiting a few minutes.

Yeah, I've tried waiting until seeing the new record being synchronized across my devices, and haven't seen the issue.

NSKeyedUnarchiveFromData is the default transformer and comes to play when you use a Core Data / SwiftData transformable attribute without explicitly specifying a transformer. Unless you are using a transformable attribute, it shouldn't be engaged.

My best guess is that your CloudKit schema / data contains something that needs a transformer (due to your historical changes?), and that triggers the error when NSPersistentCloudKitContainer tries to synchronize the data from the server to your device.

If that is the case, consider cleaning up the schema and data on the CloudKit server. Assuming you are on the CloudKit development environment, you can remove the existing schema and data by resetting the environment, and then re-create the schema by adding new records. For more information, see Initialize the CloudKit development schema and Create the CloudKit schema.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Do you have a feedback report yet? If not, I’d suggest that you file one and share your report ID here.

For a workaround, you might consider making SchedulingTime a SwiftData model, if that is appropriate, and relating it to ProtocolMedication with a too-many relationship.

You can also consider using Data directly for persistence, and providing a transient (@Transient) property for the access to the struct array (only) in memory. A transient property can't be used in a query though.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I switch to using Data with Transient computed property and it still is showing the same error logged to the console:

import SwiftData

@Model
final class ProtocolMedication {
    var uuid: UUID = UUID()
    var createdAt: Date = Date()
    var frequency: SchedulingFrequency = SchedulingFrequency.atRegularIntervals
    var interval: SchedulingInterval = SchedulingInterval(days: 1)
    var startDate: Date = Date()
    var timesData: Data?
    var dayOfWeekSelection: DayOfWeekSelection = DayOfWeekSelection(days: [1])
    var injectionRotationConfig: InjectionRotationConfig = InjectionRotationConfig()
    var medicationConcentration: MedicationConcentration = MedicationConcentration(value: nil, unit: nil)
    
    var medication: Medication?
    @Relationship(deleteRule: .cascade, inverse: \ScheduledDose.protocolMed)
    var _scheduledDoses: [ScheduledDose]?
    @Relationship(deleteRule: .cascade, inverse: \DoseLog.protocolMed)
    var _doseLogs: [DoseLog]?
    
    @Transient
    var times: [SchedulingTime] {
        get {
            guard let data = timesData else { return [] }
            return (try? PropertyListDecoder().decode([SchedulingTime].self, from: data)) ?? []
        }
        set {
            timesData = try? PropertyListEncoder().encode(newValue)
        }
    }
    
    var scheduledDoses: [ScheduledDose] {
        _scheduledDoses ?? []
    }
    
    var doseLogs: [DoseLog] {
        _doseLogs ?? []
    }
    
    init(frequency: SchedulingFrequency, interval: SchedulingInterval = SchedulingInterval(days: 1), startDate: Date, times: [SchedulingTime] = [], dayOfWeekSelection: DayOfWeekSelection = DayOfWeekSelection(days: [1]), injectionRotationConfig: InjectionRotationConfig = InjectionRotationConfig(), medicationConcentration: MedicationConcentration, medication: Medication) {
        self.frequency = frequency
        self.interval = interval
        self.startDate = startDate
        self.times = times
        self.dayOfWeekSelection = dayOfWeekSelection
        self.injectionRotationConfig = injectionRotationConfig
        self.medicationConcentration = medicationConcentration
        self.medication = medication
    }
}

Unrelated but times is a computed property so no need to mark it as Transient. You only need to do that for stored properties that shouldn't be persisted.

I'm trying to verify the issue you described, and it doesn't seem to happen to me. Here is the types I used:

struct SchedulingTime: Codable {
    let year: Int
    let month: Int
    let day: Int
}

@Model
final class Item {
    var timestamp: Date = Date.now
    var timesData: Data? = nil
    var timesData2: [SchedulingTime] = []

    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

I can successfully add a new record with the following code, without triggering the error you mentioned:

let newItem = Item(timestamp: Date())
newItem.timesData = "This is test".data(using: .utf8)
newItem.timesData2 = [SchedulingTime(year: 2025, month: 9, day: 9)]
modelContext.insert(newItem)

From CloudKit Console, I see the associated record type, CD_Item, has the following fields:

...
CD_timesData 	BYTES
CD_timesData2	BYTES
CD_timestamp	DATE/TIME

So everything looks good. I run my test app with Xcode 26.0 beta 7 + macOS Tahoe 26.0 Beta (25A5351b). Do you try with them yet? If you do and still see the issue, please provide a minimal project with detailed steps. I'd see if I can reproduce the issue with it.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Did you wait some time to make sure the error didn't pop up? It might take a minute or two. Try inserting a record then re-running and waiting a few minutes.

Yeah, I've tried waiting until seeing the new record being synchronized across my devices, and haven't seen the issue.

NSKeyedUnarchiveFromData is the default transformer and comes to play when you use a Core Data / SwiftData transformable attribute without explicitly specifying a transformer. Unless you are using a transformable attribute, it shouldn't be engaged.

My best guess is that your CloudKit schema / data contains something that needs a transformer (due to your historical changes?), and that triggers the error when NSPersistentCloudKitContainer tries to synchronize the data from the server to your device.

If that is the case, consider cleaning up the schema and data on the CloudKit server. Assuming you are on the CloudKit development environment, you can remove the existing schema and data by resetting the environment, and then re-create the schema by adding new records. For more information, see Initialize the CloudKit development schema and Create the CloudKit schema.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

SwiftData & CloudKit: Arrays of Codable Structs Causing NSKeyedUnarchiveFromData Error
 
 
Q