Swift Data issue when property implements custom Codable conformance.

I have a type which represents an ID. This ID comes from a backend as an untyped string. The Swift type looks something like this:

struct TypedID: Codable {
  init(_ string: String) {
    stringValue = string
  }
  init(from decoder: any Decoder) throws {
    stringValue = try decoder.singleValueContainer().decode(String.self)
  }
  func encode(to encoder: any Encoder) throws {
    try stringValue.encode(to: encoder)
  }
  let stringValue: String
}

If I use this type in a SwiftData @Model, or even as a property of another Codeable struct which is contained in the @Model, SwiftData fails to create instances of my model and emits the following error:

CoreData: error: CoreData: error: Row (pk = #) for entity '<@Model type>' is missing mandatory text data for property 'stringValue'

This issue goes away if I remove the custom implementation of init(from:) and encode(to:), but doing so makes the coded representation of my type an object instead of a string, which is not what I want.

/// JSON without custom implementations:
{
  "stringValue": "<the ID>"
}
/// JSON with custom implementations:
"<the ID>"

Is there anything I can do to be able to serialize a Codable type with a custom coding implementation like this to a SwiftData model?

Answered by LionelMuggeridge in 803624022

Hmm It looks like the issue is SwiftData abuses Codable (i.e. doesn't just use encode(to:) and init(from:) and the way in which it does so (which seems to include using reflection) is not robust to custom implementations of those methods.

Some of the nuance is covered by this blog post: https://fatbobman.com/en/posts/considerations-for-using-codable-and-enums-in-swiftdata-models/

That blog post also describes a trick where you can nudge SwiftData into actually using the Codable protocol correctly by storing the codable property in an Array (which did work around my issue) though it is unclear if this will be robust to future "improvements" to SwiftData...

Try RawRepresentable. Example:

struct TypedID: Codable, RawRepresentable {
    let rawValue: String

    init(rawValue: String) {
        self.rawValue = rawValue
    }

    init(_ string: String) {
        self.init(rawValue: string)
    }
}

Result:

The primary issue isn't with the JSON encoding but with SwiftData (and specifically a model defined using @Model).

RawRepresentable hits the same issue I described. If you do the standard "make a macOS app with SwiftData" flown in a recent Xcode and change the definition of Item to the following, the issue is reproducable

struct Problematic1: Codable {
  
  init(from decoder: any Decoder) throws {
    self.stringValue = try decoder.singleValueContainer().decode(String.self)
  }
  enum CodingKeys: CodingKey {
    case stringValue
  }
  
  func encode(to encoder: any Encoder) throws {
    var container = encoder.singleValueContainer()
    try container.encode(stringValue)
  }
  
  init(stringValue: String) {
    self.stringValue = stringValue
  }
  var stringValue: String
}

struct Problematic2: RawRepresentable, Codable {
  init(rawValue: String) {
    self.rawValue = rawValue
  }
  var rawValue: String
}

@Model
final class Item {
    var timestamp: Date
  
  var oneOptional: Problematic1? = Problematic1(stringValue: "1")// <-- OK
  var one: Problematic1 = Problematic1(stringValue: "1") // <-- Causes issue
  
  var twoOptional: Problematic2?  = Problematic2(rawValue: "2") // <-- OK
  var two: Problematic2 = Problematic2(rawValue: "2") // <-- Causes issue
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}
Accepted Answer

Hmm It looks like the issue is SwiftData abuses Codable (i.e. doesn't just use encode(to:) and init(from:) and the way in which it does so (which seems to include using reflection) is not robust to custom implementations of those methods.

Some of the nuance is covered by this blog post: https://fatbobman.com/en/posts/considerations-for-using-codable-and-enums-in-swiftdata-models/

That blog post also describes a trick where you can nudge SwiftData into actually using the Codable protocol correctly by storing the codable property in an Array (which did work around my issue) though it is unclear if this will be robust to future "improvements" to SwiftData...

Swift Data issue when property implements custom Codable conformance.
 
 
Q