Reputation: 1500
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)")
}
Upvotes: 3
Views: 98
Reputation: 273978
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
.
Upvotes: 3