Bithumb API JSON을 Swift 모델로 변환하는 과정에서 발생한 문제와 해결 과정 정리
Bithumb API에서 제공하는 코인 시세 데이터를 Swift의 Decodable 모델로 변환하려고 했는데, 일반적인 딕셔너리 매핑 방식이 실패함
struct BithumbTickersResponse: Decodable {
let data: [String: CoinData]
struct CoinData: Decodable {
let openingPrice: Double
let closingPrice: Double
let minPrice: Double
let maxPrice: Double
}
}
{
"data": {
"BTC": {
"opening_price": "50000000",
"closing_price": "51000000",
"min_price": "49500000",
"max_price": "51500000",
"units_traded": "100.5",
"acc_trade_value": "5050000000",
"prev_closing_price": "50500000",
"units_traded_24H": "200.2",
"acc_trade_value_24H": "10100000000",
"fluctate_24H": "1000000",
"fluctate_rate_24H": "2.0"
},
"ETH": {
"opening_price": "3500000",
"closing_price": "3550000",
"min_price": "3450000",
"max_price": "3600000",
"units_traded": "200.3",
"acc_trade_value": "710000000",
"prev_closing_price": "3500000",
"units_traded_24H": "400.6",
"acc_trade_value_24H": "1420000000",
"fluctate_24H": "50000",
"fluctate_rate_24H": "1.5"
},
......
"date": "1706700000000"
}
}
date 값은 고정된 키이므로 별도로 처리.
나머지 코인 데이터(BTC, ETH 등)는 동적 키이므로 DynamicCodingKeys를 활용하여 디코딩
struct BithumbTickersResponse: Decodable {
let data: CoinDataWrapper
struct CoinDataWrapper: Codable {
let date: String
let coins: [String: CoinData]
enum CodingKeys: String, CodingKey {
case date
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.date = try container.decode(String.self, forKey: .date) // ✅ date는 별도로 디코딩
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)
var tempCoins = [String: CoinData]()
for key in dynamicContainer.allKeys {
if key.stringValue != "date" { // "date"는 제외하고 나머지 코인 데이터만 저장
let coinData = try dynamicContainer.decode(CoinData.self, forKey: key)
tempCoins[key.stringValue] = coinData
}
}
self.coins = tempCoins
}
}
}
struct CoinData: Decodable {
let openingPrice: Double
let closingPrice: Double
let minPrice: Double
let maxPrice: Double
let unitsTraded: Double
let accTradeValue: Double
let prevClosingPrice: Double
let unitsTraded24H: Double
let accTradeValue24H: Double
let fluctate24H: Double
let fluctateRate24H: Double
enum CodingKeys: String, CodingKey {
case openingPrice = "opening_price"
case closingPrice = "closing_price"
case minPrice = "min_price"
case maxPrice = "max_price"
case unitsTraded = "units_traded"
case accTradeValue = "acc_trade_value"
case prevClosingPrice = "prev_closing_price"
case unitsTraded24H = "units_traded_24H"
case accTradeValue24H = "acc_trade_value_24H"
case fluctate24H = "fluctate_24H"
case fluctateRate24H = "fluctate_rate_24H"
}
// API에서 모든 값을 문자열로 제공하므로 Double 변환 시도
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.openingPrice = Double(try container.decode(String.self, forKey: .openingPrice)) ?? 0.0
self.closingPrice = Double(try container.decode(String.self, forKey: .closingPrice)) ?? 0.0
self.minPrice = Double(try container.decode(String.self, forKey: .minPrice)) ?? 0.0
self.maxPrice = Double(try container.decode(String.self, forKey: .maxPrice)) ?? 0.0
self.unitsTraded = Double(try container.decode(String.self, forKey: .unitsTraded)) ?? 0.0
self.accTradeValue = Double(try container.decode(String.self, forKey: .accTradeValue)) ?? 0.0
self.prevClosingPrice = Double(try container.decode(String.self, forKey: .prevClosingPrice)) ?? 0.0
self.unitsTraded24H = Double(try container.decode(String.self, forKey: .unitsTraded24H)) ?? 0.0
self.accTradeValue24H = Double(try container.decode(String.self, forKey: .accTradeValue24H)) ?? 0.0
self.fluctate24H = Double(try container.decode(String.self, forKey: .fluctate24H)) ?? 0.0
self.fluctateRate24H = Double(try container.decode(String.self, forKey: .fluctateRate24H)) ?? 0.0
}
}
JSON 데이터를 디코딩할 때, 일반적으로 키(key)는 미리 정의된 CodingKeys를 통해 매핑됩니다. 하지만, JSON의 키가 동적으로 변하는 경우에는 미리 정의할 수 없기 때문에 일반적인 방식으로 디코딩이 어렵습니다.
struct ExampleData: Decodable {
let id: Int
let name: String
enum CodingKeys: String, CodingKey {
case id, name
}
}
let jsonData = """
{
"id": 123,
"name": "Alice"
}
""".data(using: .utf8)!
let decodedData = try JSONDecoder().decode(ExampleData.self, from: jsonData)
print(decodedData.id) // 123
print(decodedData.name) // Alice
struct Item: Decodable {
let name: String
}
struct ResponseData: Decodable {
let items: [Item]
}
let jsonData = """
{
"items": [
{ "name": "Apple" },
{ "name": "Banana" }
]
}
""".data(using: .utf8)!
let decodedData = try JSONDecoder().decode(ResponseData.self, from: jsonData)
print(decodedData.items[0].name) // Apple
let jsonData = """
"Hello, world!"
""".data(using: .utf8)!
let decodedString = try JSONDecoder().decode(String.self, from: jsonData)
print(decodedString) // Hello, world!
동적 키를 처리하려면, 키 값을 미리 정의할 수 없기 때문에 DynamicCodingKeys를 사용하여 키를 동적으로 가져와야 합니다.
struct DynamicCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) { self.stringValue = stringValue }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
DynamicCodingKeys는 CodingKey 프로토콜을 준수하여 동적으로 키를 생성할 수 있도록 함. stringValue를 통해 키 값을 가져오고, intValue는 사용하지 않으므로 nil 반환
let container = try decoder.container(keyedBy: CodingKeys.self)
self.date = try container.decode(String.self, forKey: .date)
container를 생성하여 date 값을 가져옴
let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKeys.self)
dynamicContainer.allKeys를 호출하여 모든 키를 가져옴
for key in dynamicContainer.allKeys {
if key.stringValue != "date" {
let coinData = try dynamicContainer.decode(CoinData.self, forKey: key)
tempCoins[key.stringValue] = coinData
}
}
BTC, ETH 같은 키들은 DynamicCodingKeys를 통해 동적으로 디코딩됨, date는 이미 처리했으므로 제외