I'm trying to create a Codable
struct with an [any Protocol]
in Swift 5.7
struct Account: Codable {
var id: String
var name: String
var wallets: [any Wallet]
}
protocol Wallet: Codable {
var id: String { get set }
//... other codable properties
}
However, I'm getting these errors even though Wallet
conforms to Codable
Type 'Account' does not conform to protocol 'Decodable'
Type 'Account' does not conform to protocol 'Encodable'
Is it possible to make an any
conform to Codable
?
or is this still not possible with Swift 5.7?
EDIT: As answered, you have to implement your own conformance. This is how I did mine:
protocol Wallet: Identifiable, Codable {
var id: String { get set }
}
struct DigitalWallet: Wallet {
var id: String
// other properties
}
struct CashWallet: Wallet {
var id: String
// other properties
}
struct Account {
var id: String
var name: String
var wallets: [any Wallet]
}
extension Account: Codable {
enum CodingKeys: String, CodingKey {
case id
case name
case digitalWallet
case cashWallet
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
let dW = try values.decode([DigitalWallet].self, forKey: .digitalWallet)
let cW = try values.decode([CashWallet].self, forKey: .cashWallet)
wallets = []
wallets.append(contentsOf: dW)
wallets.append(contentsOf: cW)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
var digitalWallet: [DigitalWallet] = []
var cashWallet: [CashWallet] = []
wallets.forEach { wallet in
if wallet is DigitalWallet {
digitalWallet.append(wallet as! DigitalWallet)
} else if wallet is CashWallet {
cashWallet.append(wallet as! CashWallet)
}
}
try container.encode(digitalWallet, forKey: .digitalWallet)
try container.encode(cashWallet, forKey: .cashWallet)
}
}
But I have reverted to just using this instead:
struct Account: Codable {
var id: String
var name: String
var cashWallets: [CashWallet]
var digitalWallets: [DigitalWallet]
}
I worry the performance overhead of any Protocol
isn't worth it just to comply to the Dependency Inversion Principle.
CodePudding user response:
Protocols do not conform to themselves, so any Wallet
is not itself Codable, and so you won't get an automatic conformance here, and you won't get automatic handling of arrays. But you can handle it yourself without trouble:
extension Account: Encodable {
enum CodingKeys: CodingKey {
case id
case name
case wallets
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.id, forKey: .id)
try container.encode(self.name, forKey: .name)
var walletContainer = container.nestedUnkeyedContainer(forKey: .wallets)
for wallet in wallets {
try walletContainer.encode(wallet)
}
}
}
If you comment-out wallets
temporarily, Xcode 14 will auto-complete the rest of the encode(to:)
method for you, so you only need to write the loop for wallets
.
This is something that may improve in the future. There's no deep reason that Swift can't auto-generate this conformance. It just doesn't today.
However, as Alexander notes, it is not in possible to conform to Decodable in a general way because it requires an init
. You have to know which type you want to decode, and the serialized data doesn't include that information (at least the way you've described it). So you'll have to make choices that you hand-write (for example, using some default Wallet type).