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)
}
}
Это прекрасно работает ✅
Теперь я хочу сделать то же самое, но для UnkeyedDecodingContainer (чтобы декодировать массивы):
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()
}
}
Но эти переопределенные функции никогда не вызываются 🚨
P.S.
Я думаю, что ключ к ответу заключается в том, что KeyedDecodingContainer — это структура, а UnkeyedDecodingContainer — протокол.
Обновление:
Я знаю, что канонический способ сделать это:
extension Money: Decodable {
init(from decoder: Decoder) throws {
...
}
}
Но я не могу этого сделать в своей ситуации. Потому что Money уже соответствует Decodable, поэтому init(from decoder: Decoder) уже определено (он ожидает декодирования из Double, а не String). Это невозможно переопределить.
Обновление2 (минимально воспроизводимый пример):typealias Money = Dollars<Double>
где я использую крошечную стороннюю библиотеку 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"
))
}
}
Использование:
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)")
}





Давайте сначала рассмотрим, почему «переопределение» методов в KeyedDecodingContainer вообще работает. Объявленные вами методы decode не «переопределяют» существующие методы с точки зрения языка — они просто скрывают встроенные методы decode, находящиеся в другом модуле.
Когда Swift генерирует реализации Decodable для PriceData, он генерирует вызовы KeyedDecodingContainer.decode. Поскольку вы компилируете PriceData вместе с extension, встроенные методы decode скрыты. В результате эти вызовы разрешаются, они разрешаются в определенные вами методы decode.
Сгенерированный код выглядит следующим образом:
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)
}
В сгенерированном коде нет контейнера декодирования без ключа. Только после того, как .decode([Money].self) был вызван и он переходит к реализации Decodable, Array подключается.
В какой-то момент, UnkeyedDecodingContainer позвонит Array.init(from:), но этот звонок уже решен! Стандартная библиотека Swift уже скомпилирована, и она не будет знать о вашем UnkeyedDecodingContainer.decode.
Чтобы обойти эту проблему, вы можете добавить перегрузку extension в свой decode(_ type: [Money].Type).
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
}
}
Альтернативой было бы просто создать тип extension, который обертывает Decodable, вместо «переопределения» [Money].