Custom struct Codable for SwiftData

I'm encountering an issue encoding/decoding a custom struct from SwiftData. As it's all happening behind the generated code of SwiftData and a decoder, I'm not really sure what's going on.

I have a custom type defined kind of like this:

public struct Group<Key: Hashable, Element: Hashable> {
    private var elementGroups: [Element: Key]
    private var groupedElements: [Key: [Element]]
}

In short, it allows multiple elements (usually a string), to be grouped, referenced by some key.

I have Codable conformance to this object, so I can encode and decode it. For simplicity, the elementGroups is encoded/decoded, and the groupedElements is rebuilt when decoding. My implementation is similar to this:

extension Group: Codable where Key: Codable, Element: Codable {
	private enum Keys: CodingKey {
		case groups
	}

	public init(from decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: Keys.self)
		let decoded = try container.decode([Element: Key].self, forKey: .groups)

		// Enumerate over the element groups, and populate the list of elements.
		//
		var elements: [Key: [Element]] = [:]
		for group in decoded {
			elements[group.value] = (elements[group.value] ?? []) + [group.key]
		}
		
		elementGroups = decoded
		groupedElements = elements
	}

	public func encode(to encoder: Encoder) throws {
		var container = encoder.container(keyedBy: Keys.self)
		try container.encode(elementGroups, forKey: .groups)
	}
}

This works fine when encoding and decoding to JSON, but when I attempt to use this structure as a value within a SwiftData model, then decoding the type crashes my application.

@Model
final class MyModel {
	var id: UUID = UUID()
	var groups: Group<UUID, String> = Group<UUID, String>()

	init(id: UUID) {
		self.id = id
	}
}

When something attempts to decode the groups property on the MyModel object, it crashes with the following error:

Could not cast value of type 'Swift.Optional<Any>' (0x1ed7d0290) to 'Swift.Dictionary<Swift.String, Foundation.UUID>' (0x121fa9448).

I would guess that there is a nil value stored for groups in SwiftData, and attempting to decode it to a Group<UUID, String> type is failing. It's odd that it doesn't throw an exception though, and hard crashes. Also, I'm not sure why it's optional, as a value is being written out.

Does anyone have any ideas?

I enabled the arguments/environment-variables to debug what the sqllite database was doing behind the scenes, and now I'm even more confused.

TraceSQL(0x127e12970):
INSERT INTO ZVARIANTMODEL(
    Z_PK,
    Z_ENT,
    Z_OPT,
    ZPROJECT,
    ZTHUMBNAIL,
    Z2VARIANTS,
    ZASSIGNMENTS,
    ZELEMENTGROUPS,
    ZGROUPEDELEMENTS,
    ZID,
    ZMODIFIEDON,
    ZNAME,
    ZSWATCHES
) VALUES(
    1,
    4,
    1, 
    NULL,
    NULL,
    1,
    x'7b7d',
    NULL,
    NULL,
    x'31ddf4ce24b8463e9ede5bfe4457e19e',
    718570686.400954,
    'Super Long',
    x'62706c697374303...00000000000005f'
)

If I'm reading this correctly, it appears to be storing ZELEMENTGROUPS and ZGROUPEDELEMENTS which are the internal stored values on the Group type. I would have assumed it would instead just use whatever implementation I have for Codable? It is storing nil for both of these types.

This seems very odd, and not what I would expect at all.

I'm running into this same issue. Have you had any luck with it?

Ditto here. All the other advice on the subject of struct-typed model attributes asserts the struct must be Codable and it stored in a single column. The evidence shows the struct's properties are stored as individual columns, spreading the structure out and using some truly unexpected Encoder/Decoder that's not JSON. For instance, I defined

struct ComplexNumber: Codable {
    var real: Double
    var imaginary: Double
}

and

@Model
final class Item {
    var timestamp: Date
    var number1: ComplexNumber
    var number2: ComplexNumber

and got this in SQLite:

This works fine when fetching the records, but in my app it fails to decode when some of the struct's properties are Optional. I can write decoder() to deal with the optionals, but apparently SwiftData's Decoder can't cope. I suppose there's some similar limitation for Dictionary properties.

I put in FB15164782 asking for documentation on how SwiftData encodes/decodes struct-valued attributes.

Custom struct Codable for SwiftData
 
 
Q