swiftoverridingprotocolsdecodabledefault-implementation

How to override the default implementation of UnkeyedDecodingContainer protocol functions?


extension KeyedDecodingContainer {
    func decode(_: Money.Type, forKey key: Key) throws -> Money {
        let str = try decode(String.self, forKey: key)
        return try str.toMoney(on: key)
    }
    
    func decodeIfPresent(_: Money.Type, forKey key: Key) throws -> Money? {
        let str = try decodeIfPresent(String.self, forKey: key)
        return try str?.toMoney(on: key)
    }
}

This works totally fine ✅
Now I want to do exactly the same, but for UnkeyedDecodingContainer (in order to decode arrays):

extension UnkeyedDecodingContainer {
    mutating func decode(_: Money.Type) throws -> Money {
        let str = try decode(String.self)
        return try str.toMoney()
    }
    
    mutating func decodeIfPresent(_: Money.Type) throws -> Money? {
        let str = try decodeIfPresent(String.self)
        return try str?.toMoney()
    }
}

But these overridden functions are never called 🚨

P.S.
I think the key to the answer lies in the fact that KeyedDecodingContainer is a struct, while UnkeyedDecodingContainer is a protocol.

Update:
I know that a canonical way to do this is:

extension Money: Decodable {
    init(from decoder: Decoder) throws { 
    ...
    }
}

But I can't do that in my situation. Because Money already conforms to Decodable, so it already has init(from decoder: Decoder) defined (it expects to decode from Double not String). It's impossible to override it.

Update2 (minimal reproducible example):
typealias Money = Dollars<Double>
where I make use of the tiny third-party library Tagged.

extension String {
    func toMoney(on key: CodingKey? = nil) throws -> Money {
        if let money = Money(self) { return money }
        throw DecodingError.dataCorrupted(.init(
            codingPath: key.map { [$0] } ?? [],
            debugDescription: "Can't convert JSON String to Money"
        ))
    }
}

Usage:

struct PriceData: Decodable {
    let price: Money?
    let prices: [Money]
}

// NOTE: String data
let jsonData = """
{
    "price": "10.5",
    "prices": ["5.3", "7.1", "9.6"]
}
""".data(using: .utf8)!

do {
    let coin = try JSONDecoder().decode(PriceData.self, from: jsonData)
    print("Price: ", coin.price)
    print("Prices: ", coin.prices)
} catch {
    print("Error decoding JSON: \(error)")
}

Solution

  • Let's first consider why "overriding" methods in KeyedDecodingContainer works in the first place. The decode methods you declared are not "overriding" the existing methods as far as the language is concerned - they are just hiding the built-in decode methods that are in a different module.

    When Swift generates Decodable implementations for PriceData, it generates calls to KeyedDecodingContainer.decode. Since you are compiling PriceData together with the extension, the built-in decode methods are hidden. As a result these calls are resolved, they resolve to the decode methods you defined.

    The generated code looks like this:

    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
    
        // this call will resolve to the deocdeIfPresent you defined
        self.price = try container.decodeIfPresent(Money.self, forKey: .price)
    
        // this call will resolve to the built-in decode method
        self.prices = try container.decode([Money].self, forKey: .prices)
    }
    

    There is no unkeyed decoding container involved in the generated code. It is only after .decode([Money].self) has been called, and it goes into the Decodable implementation of Array, does an UnkeyedDecodingContainer get involved.

    At some point, Array.init(from:) would call UnkeyedDecodingContainer.decode, but this call has already been resolved! The Swift standard library has already been compiled, and it wouldn't know about your extension.

    To work around this, you can add a decode(_ type: [Money].Type) overload in your extension.

    extension KeyedDecodingContainer {
        func decode(
            _ type: [Money].Type,
            forKey key: Key
        ) throws -> [Money] {
            var unkeyedContainer = try nestedUnkeyedContainer(forKey: key)
            var result = [Money]()
            while !unkeyedContainer.isAtEnd {
                let str = try unkeyedContainer.decode(String.self)
                result.append(try str.toMoney())
            }
            return result
        }
    }
    

    An alternative would be to just create a Decodable type that wraps a [Money], instead of "overriding" KeyedDecodingContainer.decode.