Decodable, protocol conformance, and generics: how to "dumb down" a conforming type to call the right implementation

You probably don't need new function on KeyedDecodingContainer. Synthesizer won't know about it so you won't be able to utilize default synthesis. It'd be easier to "wrap" every variable that support migration with type that handles fallback. Property wrapper should be a good fit.

@propertyWrapper
struct Migratable<Value: Decodable>: Decodable {
    var wrappedValue: Value
    
    init(from decoder: Decoder) throws {
        if let decoded = try? Value(from: decoder) {
            wrappedValue = decoded
        } else {
            // Setup from __AnonymousObjC here
            throw NSError()
        }
    }
}

extension Migratable: Encodable where Value: Encodable {
    func encode(to encoder: Encoder) throws {
        try wrappedValue.encode(to: encoder)
    }
}

So you can do

struct Test: Codable {
    @Migratable var value: Int
}

let data = #"{"value": 77}"#.data(using: .utf8)!
let decoder = JSONDecoder()
let encoder = JSONEncoder()

// Don't wrap top-level. Test(value: 77)
let test = try decoder.decode(Test.self, from: data)
// Wrap top-level as well. Test(value: 77)
let wrappedTest = try decoder.decode(Migratable<Test>.self, from: data).wrappedValue

try String(data: encoder.encode(test), encoding: .ascii) // #"{"value":77}"#

One problem would be if you want to have synthesizer uses decodeIfPresent, which is not something property wrapper supported (see discussion here). In which case, you need to manually implement init(from:).

1 Like