-
-
Notifications
You must be signed in to change notification settings - Fork 812
Description
What did you do?
AttributedString structs are encodable using an @CodableConfiguration property wrapper provided with the subset of attributes that is allowed to be encoded into JSON.
I would expect GRDB to be able to handle encoding these values as JSON columns, but a subtlety in how CodableWithConfiguration types makes that impossible with the current implementation, but only when the field is optional.
Try this sample code:
import Foundation
import GRDB
let dbQueue = try DatabaseQueue()
var migrator = DatabaseMigrator()
migrator.registerMigration("createPosts") { db in
try db.create(table: "post") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull()
t.column("date", .date).notNull()
t.column("text", .jsonText)
}
}
try migrator.migrate(dbQueue)
struct Post: PersistableRecord, FetchableRecord, Hashable, Codable {
var id: Int
var title: String
var date: Date
@CodableConfiguration(from: \.foundation)
var text: AttributedString? = nil
}
let post = Post(
id: 0,
title: "My first post.",
date: Date(),
text: try? AttributedString(markdown: "This is my **first** post!")
)
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let postJSON = String(data: try jsonEncoder.encode(post), encoding: .utf8)!
print(postJSON)
try dbQueue.inTransaction { db in
try post.insert(db) // <-- crash here
return .commit
}What did you expect to happen?
Expected the Post record to be inserted to the database.
What happened instead?
The program crashes by hitting one of these fatalErrors:
EncodableRecord+Encodable.swift:33
func unkeyedContainer() -> UnkeyedEncodingContainer {
fatalError("unkeyed encoding is not supported")
}or
EncodableRecord+Encodable.swift:48
func singleValueContainer() -> SingleValueEncodingContainer {
// [...]
fatalError("single value encoding is not supported")
}This is because the AttributedString's encoding implementation will either open a singleValueContainer(), or an unkeyedContainer() depending on its internal representation, and this is getting called on the top level RecordEncoder rather than on an encoding container for a JSON value.
Note that this only happens when the AttributedString field is optional.
It appears to be related to how CodableWithConfiguration works when the field is optional:
public extension KeyedEncodingContainer {
mutating func encode<T, C>(_ wrapper: CodableConfiguration<T?, C>, forKey key: Self.Key) throws {
switch wrapper.wrappedValue {
case .some(let val):
try val.encode(to: self.superEncoder(forKey: key), configuration: C.encodingConfiguration)
break
default: break
}
}
}This code is calling self.superEncoder(forKey: key), but the RecordEncoder.KeyedEncoder always returns recordEncoder for that method, when it should probably return something wrapping a JSON encoder. I have no idea why it's asking for a superEncoder, but this seems to be the way they've implemented it.
Environment
GRDB flavor(s): GRDB
GRDB version: 6.25.0
Installation method: SPM
Xcode version: Xcode 15
Swift version: 5.10
Platform(s) running GRDB: macOS
macOS version running Xcode: 14.3.1