marcoarment / Blackbird

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Custom CodingKeys

robertmryan opened this issue · comments

Consider this model object:

struct Meal: BlackbirdModel, Identifiable {
    @BlackbirdColumn var id: String
    @BlackbirdColumn var name: String
    @BlackbirdColumn var thumbnailUrl: URL

    enum CodingKeys: String, CodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case thumbnailUrl = "strMealThumb"
    }
}

For the sake of decoding a network resource with questionable naming conventions, I have introduced a CodingKeys to map their names to my own. Fine.

Blackbird creates a table with names of id, name, and thumbnailUrl. That seems good to me. But, when I try to save a record I get the following fatal error message:

2023-04-22 15:27:10.868259-0700 MyApp[49584:3615565] Blackbird/BlackbirdModel.swift:829: Fatal error: Table "Meal" definition defaults do not decode to model Meal: missingValue("idMeal")

Note, the database has the column name, but validateSchema(database:core:) is looking for strMeal (the name from CodingKeys). It looks like the logic it used for creating the column names (using the actual property names) is different than what validateSchema(database:core:) used. It strikes me that we should either use CodingKeys everywhere, or not at all. (I'd vote for the latter.)

Thoughts?

Well, I got it working. I kinda can't believe what it took. But I think it works!

Thanks.

@marcoarment, thank you for jumping that so quickly. That fixed the custom coding keys problem. (I was helping someone on this Stack Overflow question.)

FWIW, I tried a rendition with a custom decoder and things fell apart again, with this error:

Fatal error: … instances cannot be generated by simple decoding

Based upon the text of the error message, I gather that this is a known issue, so I won’t bother you further. But, if you want me to post a new issue with a MCVE, let me know. I am happy either way.

Thanks again.

Actually, I’ll just document it here so I don’t lose my train of thought. Bottom line, this works:

struct Meal: BlackbirdModel, Identifiable {
    @BlackbirdColumn var id: String
    @BlackbirdColumn var name: String
    @BlackbirdColumn var thumbnail: URL

    enum CodingKeys: String, BlackbirdCodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case thumbnail = "strMealThumb"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        self._id = try container.decode(BlackbirdColumn<String>.self, forKey: .id)
        self._name = try container.decode(BlackbirdColumn<String>.self, forKey: .name)
        self._thumbnail = try container.decode(BlackbirdColumn<URL>.self, forKey: .thumbnail)
    }
}

But the following results in the aforementioned “simple decoding” fatal error:

struct Meal: BlackbirdModel, Identifiable {
    @BlackbirdColumn var id: Int
    @BlackbirdColumn var name: String
    @BlackbirdColumn var thumbnail: URL

    enum CodingKeys: String, BlackbirdCodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case thumbnail = "strMealThumb"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let string = try container.decode(String.self, forKey: .id)
        guard let value = Int(string) else {
            throw DecodingError.dataCorruptedError(forKey: .id, in: container, debugDescription: "Expected string representation of integer")
        }
        self._id = BlackbirdColumn<Int>(wrappedValue: value)
        self._name = try container.decode(BlackbirdColumn<String>.self, forKey: .name)
        self._thumbnail = try container.decode(BlackbirdColumn<URL>.self, forKey: .thumbnail)
    }
}

I haven't gone through it in any detail, but it feels like the problem is the overloading of the CodingKeys for both JSON and Blackbird.

Ah, I see. This is a trickier issue.

Blackbird needs a way to initialize empty instances of any BlackbirdModel struct, so it uses a Decoder trick (EmptyDecoder in the code) to automatically decode empty/zero default values for each data type.

In this case, the custom init method is asking for a String for .id, which it expects to convert to an Int manually.

But when Blackbird's EmptyDecoder is asked for a String, it returns an empty string (""), which fails the Int() conversion in this model and throws this error.

I can't think of a good way around this problem at the moment, other than not fighting it and simply requiring that any BlackbirdModel implementing a custom init(from decoder: Decoder) method must work when the decoder provides empty strings, zeros for numbers, etc. — or checks to see if decoder is an EmptyDecoder and provides a valid instance by some other method if so.

I've just improved the error message to reference the documentation about this feature, which now exists.

The special decoder used by Blackbird is now a public type (BlackbirdDefaultsDecoder) that can get special handling in client custom-decoding methods, so the above code can now be:

if decoder is BlackbirdDefaultsDecoder {
    self.id = 0
} else {
    let idStr = try container.decode(String.self, forKey: .id)
    guard let id = Int(idStr) else {
        throw DecodingError.dataCorruptedError(forKey: .id, in: container, debugDescription: "Expected numeric string")
    }
    self.id = id
}

self.name = try container.decode(String.self, forKey: .name)
self.thumbnail = try container.decode(URL.self, forKey: .thumbnail)

Note also that you can decode BlackbirdColumn values in a straightforward way using their wrapped type like I've done here — there's no need to decode the BlackbirdColumn<T> wrappers directly into the _-prefixed local variables.

Excellent.

FWIW, that decode logic with the _-prefixed variables was synthesized for me in the auto-complete process in Xcode 14.3. I didn't type that, but Xcode did. So I just accepted what it gave me, as it worked (until I tried changing a type in the manual unwrapping process). I should have questioned that auto-complete code…