🍁

Swift: any Protocolの配列をCodableに対応させる

2024/10/21に公開
protocol Fruit: Codable {}

struct Apple: Fruit {}
struct Banana: Fruit {}
struct Cherry: Fruit {}

struct Cart: Codable {
    var fruits: [any Fruit]
}

例えばこのような状態で、CartJSONDecoderJSONEncoderでデコード/エンコードできるようにします。

まず、デコードするときに真の型がなんなのかわかるようにするために、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>をデコードできるようにするために、KeyedDecodingContainerUnkeyedDecodingContainerに関数を生やします。

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>をエンコードできるようにするために、KeyedEncodingContainerUnkeyedEncodingContainerに関数を生やします。

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