なるようになるかも

力は多くの場合、その人の思いを超えない。

JSONDecoder で型がごちゃ混ぜの JSON 配列をデコードする

ルートが配列で、最初の要素はその後の要素の個数を表す、という JSON があったときに、どういう Decodable を書けばいいのか?で少し悩んだので。

[
{"count" : 5 },
{"name" : "itemA", "value" : "valueA" },
{"name" : "itemB", "value" : "valueB" },
{"name" : "itemC", "value" : "valueC" },
{"name" : "itemD", "value" : "valueD" },
{"name" : "itemE", "value" : "valueE" }
]

それぞれの要素が、以下のいずれかの型に decode されるというところまでは共通。

struct Count: Codable {
    let count: Int
}

struct Item: Codable {
    let name: String
    let value: String
}

方法1. 取り得る値の両方でパースする

Stack Overflow の回答で見つけたのが、singleValueContainer() でそれぞれの型に対して decode() できるかチェックする方法。

enum Response: Decodable {
    case count(Count)
    case item(Item)
    
    init(from decoder: Decoder) throws {
        if let count = try? decoder.singleValueContainer().decode(Count.self) {
            self = .count(count)
            return
        }
        if let item = try? decoder.singleValueContainer().decode(Item.self) {
            self = .item(item)
            return
        }
        fatalError()
    }
}

singleValueContainer() のリファレンスを見ると、

Returns the data stored in this decoder as represented in a container appropriate for holding a single primitive value.

「単一のプリミティブな値を持つ」というような説明があって、SO の回答でも IntString を decode していたので、ES の仕様でいうところの Primitive Data Types しか decode できないのだと勝手に勘違いしていたけど、Decodable でも普通にいけた。

方法2. 配列を直接全部走査する

このケースだと、「先頭の要素は全件のカウントが入っている」ということが自明なので、unkeyedContainer() で配列のコンテナを取得して、要素一つ一つに対して decode() していくという方法の方が使い勝手が良かった。

struct Response: Decodable {
    let count: Int
    let items: [Item]
    
    init(from decoder: Decoder) throws {
        var unkeyedContainer = try decoder.unkeyedContainer()
        
        count = try unkeyedContainer.decode(Count.self).count
        
        var result: [Item] = []
        result.reserveCapacity(count)
        
        while !unkeyedContainer.isAtEnd {
            result.append(try unkeyedContainer.decode(Item.self))
        }
        items = result
    }
}

JSON の構造が固定ならこちらの方がよさそう。

逆の操作をすれば encode() もできるので、Codable にすることもできる。

extension Response: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(Count(count: count))
        try items.forEach { try container.encode($0) }
    }
}