🍁
Swift: any Protocolの配列をCodableに対応させる
protocol Fruit: Codable {}
struct Apple: Fruit {}
struct Banana: Fruit {}
struct Cherry: Fruit {}
struct Cart: Codable {
var fruits: [any Fruit]
}
例えばこのような状態で、Cart
をJSONDecoder
とJSONEncoder
でデコード/エンコードできるようにします。
まず、デコードするときに真の型がなんなのかわかるようにするために、enum
で種類を定義します。
enum FruitType: String, Codable {
case apple
case banana
case cherry
}
protocol Fruit: Codable {
var type: FruitType { get }
}
また、Apple, Banana, Cherryのそれぞれで異なるプロパティを持っていてもデコード/エンコードできるようにするために、CodingKey
を定義します。
struct Apple: Fruit {
var type = FruitType.apple
var weight: Double
}
struct Banana: Fruit {
var type = FruitType.banana
var length: Int
}
struct Cherry: Fruit {
var type = FruitType.cherry
var number: Int
}
enum FruitCodingKeys: CodingKey {
case type
case weight
case length
case number
}
Array<any Fruit>
をデコードできるようにするために、KeyedDecodingContainer
とUnkeyedDecodingContainer
に関数を生やします。
extension KeyedDecodingContainer {
func decode(_ type: Array<any Fruit>.Type, forKey key: K) throws -> Array<any Fruit> {
var container = try nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<any Fruit>.Type) throws -> Array<any Fruit> {
var array = [any Fruit]()
while isAtEnd == false {
let container = try nestedContainer(keyedBy: FruitCodingKeys.self)
let type = try container.decode(FruitType.self, forKey: .type)
switch type {
case .apple:
let weight = try container.decode(Double.self, forKey: .weight)
array.append(Apple(weight: weight))
case .banana:
let length = try container.decode(Int.self, forKey: .length)
array.append(Banana(length: length))
case .cherry:
let number = try container.decode(Int.self, forKey: .number)
array.append(Cherry(number: number))
}
}
return array
}
}
Array<any Fruit>
をエンコードできるようにするために、KeyedEncodingContainer
とUnkeyedEncodingContainer
に関数を生やします。
extension KeyedEncodingContainer {
mutating func encode(_ value: Array<any Fruit>, forKey key: K) throws {
var container = nestedUnkeyedContainer(forKey: key)
try container.encode(value)
}
}
extension UnkeyedEncodingContainer {
mutating func encode(_ value: Array<any Fruit>) throws {
try value.forEach { element in
var container = nestedContainer(keyedBy: FruitCodingKeys.self)
try container.encode(element.type, forKey: .type)
switch element {
case let e as Apple:
try container.encode(e.weight, forKey: .weight)
case let e as Banana:
try container.encode(e.length, forKey: .length)
case let e as Cherry:
try container.encode(e.number, forKey: .number)
default:
break
}
}
}
}
最後に、生やした関数を使ってCart
のデコード/エンコード処理を書きます。
struct Cart: Codable {
var fruits: [any Fruit]
enum CodingKeys: CodingKey {
case fruits
}
init(fruits: [any Fruit]) {
self.fruits = fruits
}
init(from decoder: any Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
fruits = try values.decode(Array<any Fruit>.self, forKey: .fruits)
}
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(fruits, forKey: .fruits)
}
}
これでany Fruit
の配列をデコード/エンコードできるようになりました。
let cart = Cart(fruits: [
Apple(weight: 5.4),
Banana(length: 11),
Cherry(number: 2)
])
do {
let data = try JSONEncoder().encode(cart)
let restoredCart = try JSONDecoder().decode(Cart.self, from: data)
print(restoredCart)
} catch {
print(error.localizedDescription)
}
Discussion