Swiftで 数字がキーになっているJSONデータをデシリアライズする

下記のように店舗情報のキーが数字になっているJSONデータをデシリアライズできるか?と質問があり回答しました。

{
    "response": {
        "total_hit_count": 4,
        "0": {
            "shop_id": "6072772",
            "shop_name": "店舗A"
        },
        "1": {
            "shop_id": "6072773",
            "shop_name": "店舗B"
        },
        "2": {
            "shop_id": "6072774",
            "shop_name": "店舗C"
        },
        "3": {
            "shop_id": "6072775",
            "shop_name": "店舗D"
        }
    }
}

そもそもとして店舗情報は配列で欲しい……と気持ちがあります。ぐるなびAPIらしいです。

解決編

CodingKeyを継承したキーリストは固定値(enum)である必要はないので、下記のように定義することができます。

struct CodingKeys: CodingKey {
    var stringValue: String
    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    var intValue: Int?
    init?(intValue: Int) {
        return nil
    }
}

コードの全体としては下記の通りです。このAPIの数字部分がどこまで伸びるか仕様を把握できていないので、仮に 0 〜 total_hit_count までの数字のキーがあると想定しています。

import Foundation

struct Reviews: Decodable {
    let response: RakutenResponse
}

struct RakutenResponse: Decodable {
    let total_hit_count: Int
    let reviews: [Review]
    
    // MARK: - codable
    
    struct CodingKeys: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
        
        // 固定で返ってくるキー
        static let total_hit_count = CodingKeys(stringValue: "total_hit_count")!
    }
  
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        total_hit_count = try values.decode(Int.self, forKey: .total_hit_count)

        // NOTE: 数字部分の仕様がわからないので 0 〜 hit_count までデータを取得する
        reviews = try (0 ..< total_hit_count)
            .map { (index) -> Review? in
                try values.decodeIfPresent(Review.self, forKey: CodingKeys(stringValue: "\(index)")!)
            }
            .compactMap { $0 }
    }
}

struct Store: Decodable {
    let shop_id: String
    let shop_name: String
}