【Swift】動的なキーを持つJSONをCodableする方法

Swift

はじめに

Swiftでアプリ開発のお仕事をしている際にapiからのデータをCodableでデコードしていたのですが、今回JSONキーが動的に変わるパターンを実装したので参考になればと思います。

デーコードするJSON

Calendar直下にyyyyMMdd形式で取得した際に動的に変わる日付をキーを持ち、その下にcountという値を持った構造になっています。

{
    "status": "OK",
    "calendar": {
        "20210501": {
            "count": 1
        },
        "20210615": {
            "count": 2
        },
        "20210720": {
            "count": 3
        }
    }
}

モデルクラス

struct RootNode: Codable {
    let status: String?
    let calendar: [CalendarObj]?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: RootCodingKey.self)
        self.status = try container.decode(String.self, forKey: .status)
        
        // キーが年月日で動的な値のためCodingKeyをyyyyMMdddで作成し、
        // 取得したノードから値を取り出してインスタンスの配列を作成
        let calendarContainer = try container.nestedContainer(keyedBy: RootCodingKey.self, forKey: .calendar)
        var _calendar: [CalendarObj] = []
        try calendarContainer.allKeys.forEach { key in
            // CodingKeyをyyyyMMddで作成し、ノードを取得
            let calendarContainer = try calendarContainer.nestedContainer(
                keyedBy: CalendarCodingKey.self, forKey: RootCodingKey(stringValue: key.stringValue)!
            )
            // Calendar以下の値を取得
            let count = try calendarContainer.decode(Int.self, forKey: CalendarCodingKey.count)
            _calendar.append(CalendarObj(date: key.stringValue, count: count))
        }
        self.calendar = _calendar
    }
    
    private struct RootCodingKey: CodingKey {
        var stringValue: String
        
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        
        var intValue: Int?
        
        init?(intValue: Int) {
            self.stringValue = "\(intValue)"
            self.intValue = intValue
        }
        
        static let status = RootCodingKey(stringValue: "status")!
        static let calendar = RootCodingKey(stringValue: "calendar")!
    }
    
    private enum CalendarCodingKey: String, CodingKey {
        case count
    }
}

struct CalendarObj: Codable {
    let date: String?
    let count: Int?
    
    init(date: String, count: Int) {
        self.date = date
        self.count = count
    }
}

nestedContainerCalendarキーに対応するノードを取得します。

let calendarContainer = try container.nestedContainer(keyedBy: RootCodingKey.self, forKey: .calendar)

その直下にあるallKeysの配列から、キー(yyyyMMddd)を順番に取り出して、その値をCodingKeyとして配下にあるノードを取得します。

try calendarContainer.allKeys.forEach { key in
  // ノードの中身を取得
}

あとはデコードで個別の値を取得したものを使ってCalendarObjのインスタンスを生成し、配列に格納しています。

let count = try calendarContainer.decode(Int.self, forKey: CalendarCodingKey.count)
_calendar.append(CalendarObj(date: key.stringValue, count: count))

呼び出し元

guard let url = Bundle.main.url(forResource: "Calendar", withExtension: "json") else {
    fatalError("url not found.")
}

guard let data = try? Data(contentsOf: url) else {
    fatalError("data not found.")
}

let decoder = JSONDecoder()
do {
    let rootNode: RootNode = try decoder.decode(RootNode.self, from: data)
    print("result data:\(rootNode)")
} catch {
    print(error.localizedDescription)
}

まとめ

Codableでデコードがどのようにされているかあまり意識していなかったので、最初はちょっと分かりづらかったですが、xmlのパースと同じでノードを上からしたに下って値を取得していくだけなので慣れればそれほど難しくはないかなと思います。

参考

【Swift】Codableで動的なキーを持つJSONに対応する | DevelopersIO
Swiftで動的なキーを持つJSONに対応できるCodableなモデルの実装方法について調べてみました。

コメント

タイトルとURLをコピーしました