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

I am working on migrating a data storage library from Objective-C to Swift, with Codable, and I'm running into a problem with how to support migration from "anonymous" (embedding) types to Codable. To support the migration, First, I need to migrate a little bit of pre-existing generic metadata when decoding each old type. Then, what I would like to do is make that intermediate step invisible to the client, so that init(from:) does not need to be implemented by hand.

The first part I can do, if I implement a custom method on the KeyedDecodingContainer, similar to this:

public extension KeyedDecodingContainer { 
    func decodeAnonymousIfPresent<T: Codable>( _ codable: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? {
    if let decoded = try ? decodeIfPresent(T.self, forKey: key) {
        return decoded
    } else if let anonymousObject = try ? decodeIfPresent(__AnonymousObjC.self, forKey: key) {
        // code to migrate metadata from the old type to the new one snipped
    }
}

And in the caller, I can use this in an init(from:) method:

required init (from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self )
    let address = try container.decodeAnonymousIfPresent(SwiftyAddress.self , forKey: .address)
    /// etc.
}

If the old data is present, it will migrate to the new object (via generic metadata shenanigans), and if the new data is present, the new object will initialize directly.

The trouble is, I have a lot of data types, and I'd rather not implement init(from:) methods for the ones that can get away without one, just for the sake of migration.

The current path I am on, is to introduce a new, empty protocol to support the migration, for example:

public protocol Migratable: Codable { }

Then, instead of implementing init(from:), just declare conformance for each type, like this:

extension SwiftyAddress: Migratable { }

Then, I'd want to modify the KeyedDecodingContainer methods to map directly to the decode implementations, like this:

public extension KeyedDecodingContainer { 
    func decodeIfPresent<T: Migratable>( _ migratable: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? {
    if let decoded = try ? decodeIfPresent(T.self, forKey: key) {
        return decoded
    } else if let anonymousObject = try ? decodeIfPresent(__AnonymousObjC.self, forKey: key) {
    // code to migrate metadata from the old type to the new one snipped
    }
}

What I need for this to work, however, is to fix the line if let decoded = try ? decodeIfPresent(T. self , forKey: key) to decode a T: Codable, not the T: Migratable, so that it calls the Codable decode method. (As written, it goes into infinite recursion.) Is such a thing possible?

If anyone has suggestions on how to continue down this path, or other alternative paths to explore, please let me know. Thanks in advance for your help!

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

Thanks for this suggestion for another path to explore, this is huge!

To further note, the details of property wrapper is described in SE-0258, which is accepted with modification.

It also support certain initialization syntax such as.

@Migratable(arg2: value2, arg3: value3) var foo: Int = 3
// Equivalent to
// var _foo = Migratable(wrappedValue: 3, arg2: value2, arg3: value3)

@Migratable var foo: Bar = Bar(...)
// var _foo = Migratable(wrappedValue: Bar(...))

So if you want any of that, you can customarily add init.

1 Like

@mrlegowatch I'm putting the finishing touches on CodableWrappers which makes this a bit simpler to implement.

public struct MigratingDecoder<T: Decodable>: StaticDecoder {

    public static func decode(from decoder: Decoder) throws -> T {
        if let decoded = try? T(from: decoder) {
            return decoded
        } else {
            // Setup from __AnonymousObjC here
            throw NSError()
        }
    }
}

struct MyStruct: Codable {
    // Set up like this, you will only override the Decoding and use the default Encoding
    @CustomDecoding<MigratingDecoder> var value: Int
}
2 Likes

I'll answer the original question, since it's occasionally a useful pattern: the only way to do this in current Swift is to use a separate function with fewer constraints on the generic parameter:

public extension KeyedDecodingContainer { 
    private func decodeIfPresentDefault<T: Codable>( _ codable: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? {
        return try decodeIfPresent(codable, forKey: key)
    }

    func decodeIfPresent<T: Migratable>( _ migratable: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T? {
        if let decoded = try decodeIfPresentDefault(T.self, forKey: key) {
            return decoded
        } else if let anonymousObject = try decodeIfPresentDefault(__AnonymousObjC.self, forKey: key) {
        // code to migrate metadata from the old type to the new one snipped
        }
    }
}

(That function doesn't have to live on the same extension either, but that seemed most convenient here.)

2 Likes