Swift解析RetroAchievements API JSON:如何获取多类型属性原始值?
解决RetroAchievements API混合类型属性的直接访问问题
问题场景
在使用RetroAchievements API构建Swift Package时,API返回的JSON中部分数值属性(如NumAchieved、NumAchievedHardcore)存在混合类型:有时是Int,有时是String。通过Quicktype生成的模型用Achieved枚举包装了这些值,导致访问属性时得到的是string("15")或integer(0)这类枚举包装结果,无法直接获取原始的Int/String值。
解决方案
方案1:给枚举添加计算属性(保留原始类型信息)
修改Achieved枚举,新增计算属性直接提取Int或String值,同时保留原始的枚举类型支持:
enum Achieved: Codable { case integer(Int) case string(String) // 获取Int值:字符串类型会尝试转换为Int,转换失败返回nil var intValue: Int? { switch self { case .integer(let value): return value case .string(let str): return Int(str) } } // 获取String值:Int类型会转换为字符串 var stringValue: String { switch self { case .integer(let value): return String(value) case .string(let str): return str } } // 原有解码、编码逻辑保持不变 init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let x = try? container.decode(Int.self) { self = .integer(x) return } if let x = try? container.decode(String.self) { self = .string(x) return } throw DecodingError.typeMismatch(Achieved.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Achieved")) } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .integer(let x): try container.encode(x) case .string(let x): try container.encode(x) } } }
使用方式:
// 获取Int值 if let numAchieved = decodedData.recent.joined().first?.numAchieved.intValue { print(numAchieved) // 输出:15 } // 获取String值 let numAchievedStr = decodedData.recent.joined().first?.numAchieved.stringValue ?? "" print(numAchievedStr) // 输出:"15"
方案2:统一解码为Int(更简洁,适合数值场景)
由于API返回的这些混合类型值本质都是数值,可直接修改模型解码逻辑,将所有这类属性统一解析为Int类型,彻底去掉枚举包装:
struct Recent: Codable { let gameID, consoleID, consoleName, title: String let imageIcon, lastPlayed: String let numPossibleAchievements, possibleScore: Int let numAchieved, scoreAchieved: Int let numAchievedHardcore, scoreAchievedHardcore: Int enum CodingKeys: String, CodingKey { case gameID = "GameID" case consoleID = "ConsoleID" case consoleName = "ConsoleName" case title = "Title" case imageIcon = "ImageIcon" case lastPlayed = "LastPlayed" case numPossibleAchievements = "NumPossibleAchievements" case possibleScore = "PossibleScore" case numAchieved = "NumAchieved" case scoreAchieved = "ScoreAchieved" case numAchievedHardcore = "NumAchievedHardcore" case scoreAchievedHardcore = "ScoreAchievedHardcore" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // 解码普通字符串属性 gameID = try container.decode(String.self, forKey: .gameID) consoleID = try container.decode(String.self, forKey: .consoleID) consoleName = try container.decode(String.self, forKey: .consoleName) title = try container.decode(String.self, forKey: .title) imageIcon = try container.decode(String.self, forKey: .imageIcon) lastPlayed = try container.decode(String.self, forKey: .lastPlayed) // 定义通用解码函数:先尝试Int,失败则解码String转Int func decodeInt(forKey key: CodingKeys) throws -> Int { if let intValue = try? container.decode(Int.self, forKey: key) { return intValue } let stringValue = try container.decode(String.self, forKey: key) guard let intValue = Int(stringValue) else { throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: container.codingPath + [key], debugDescription: "无法将字符串转换为整数")) } return intValue } // 解码所有数值属性 numPossibleAchievements = try decodeInt(forKey: .numPossibleAchievements) possibleScore = try decodeInt(forKey: .possibleScore) numAchieved = try decodeInt(forKey: .numAchieved) scoreAchieved = try decodeInt(forKey: .scoreAchieved) numAchievedHardcore = try decodeInt(forKey: .numAchievedHardcore) scoreAchievedHardcore = try decodeInt(forKey: .scoreAchievedHardcore) } }
使用方式:
直接访问Int属性即可,无需额外处理:
print(decodedData.recent.joined().first!.numAchieved) // 输出:15
方案选择
- 如果需要保留原始数据类型(比如要区分API返回的是字符串还是数字),选择方案1;
- 如果业务上这些属性都是数值,不需要区分原始类型,方案2更简洁,代码维护成本更低。
内容的提问来源于stack exchange,提问作者user11640506
相关产品推荐
相关产品推荐

