I'm encountering an error when trying to save a SwiftData entity with a transformable attribute that should handle an array of strings. The error occurs specifically when the array is empty.
The Error
Thread 1: "Unacceptable type of value for attribute: property = \"categories\"; desired type = NSData; given type = Swift.__EmptyArrayStorage; value = (\n)."
This happens when calling try sharedModelContainer.mainContext.save() after inserting a CachedFood entity with an empty categories array.
Code Overview
I have a CachedFood model with a transformable categories property:
@Model
final class CachedFood {
@Attribute(.unique) var id: String
@Attribute(.transformable(by: "CategoriesTransformer"))
var categories: [String]
init(from food: Food) {
self.id = food.id
self.categories = food.category.isEmpty ? [] : food.category
}
}
The transformer is defined as:
@objc(CategoriesTransformer)
final class CategoriesTransformer: ValueTransformer {
override class func transformedValueClass() -> AnyClass {
return NSData.self
}
override class func allowsReverseTransformation() -> Bool {
return true
}
override func transformedValue(_ value: Any?) -> Any? {
let array: [String]
if let value = value as? [String] {
array = value
} else {
array = []
}
return try? JSONEncoder().encode(array)
}
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else {
return [String]()
}
return (try? JSONDecoder().decode([String].self, from: data)) ?? [String]()
}
}
The Problem
When I create a CachedFood with an empty categories array (like in the Food.dummy case), I get the error about unacceptable type. The error suggests SwiftData is getting a Swift.__EmptyArrayStorage type when it expects NSData. The transformer doesn't seem to be called for empty arrays, as I don't see the print statements from the transformer methods.
Why is the transformer not being called for empty arrays, and how can I properly handle transformable attributes that might be empty arrays in SwiftData?
Sample code to run:
// MARK: - App
@main
struct TestDataPlayApp: App {
var sharedModelContainer: ModelContainer = {
// Verify transformer is registered first
print("Registering transformers...")
registerGlobalTransformers()
let schema = Schema([
CachedFood.self,
])
// Verify schema
print("Creating schema with entities:", schema.entities.map { $0.name })
let modelConfiguration = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
let container = try ModelContainer(
for: schema,
configurations: [modelConfiguration]
)
// Verify container creation
print("Model container created successfully")
print("Container schema:", container.schema)
return container
} catch {
fatalError("Failed to create model container: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
let cachedFood = CachedFood(from: .dummy)
sharedModelContainer.mainContext.insert(cachedFood)
do {
try sharedModelContainer.mainContext.save()
} catch {
print("Failed to cache food: \(error)")
}
}
}
.modelContainer(sharedModelContainer)
}
}
func registerGlobalTransformers() {
// Register each transformer with its unique name
ValueTransformer.setValueTransformer(
CategoriesTransformer(),
forName: NSValueTransformerName("CategoriesTransformer")
)
}
// MARK: - Food App domain entity
struct Food: Identifiable, Codable {
var id: String
var category: [String]
}
// MARK: - Dummy food data for testing
extension Food {
static let dummy = Food(id: "123", category: [])
}
// MARK: - Empty View
struct ContentView: View {
var body: some View {
EmptyView()
}
}