Swift调用YouTube API时JSON解码报错:videoId字段缺失
问题描述
作为Swift编程新手,在跟随教程调用YouTube API获取视频时,编写了以下请求代码:
func getMovie(with query: String, completion: @escaping (VideoElement) -> Void) { guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else { return } guard let url = URL(string: "\(Constants.Youtube_Base_URL)q=\(query)&key=\(Constants.Youtube_API_KEY)") else { return } let task = URLSession.shared.dataTask(with: URLRequest(url: url)) { data, _, error in guard let data = data, error == nil else { return } do { let results = try JSONDecoder().decode(YoutubeSearchResponse.self, from: data) print(results) } catch { print(error) } } task.resume() }
对应的Codable数据模型:
import Foundation struct YoutubeSearchResponse: Codable { let items: [VideoElement] } struct VideoElement: Codable { let kind: String let etag: String let id: IdVideoElement } struct IdVideoElement: Codable { let kind: String let videoId: String }
API返回的JSON示例(简化版):
{ "kind": "youtube#searchListResponse", "items": [ { "kind": "youtube#searchResult", "id": { "kind": "youtube#video", "videoId": "H5v3kku4y6Q" } }, { "kind": "youtube#searchResult", "id": { "kind": "youtube#video", "videoId": "bt_pJG3YZpc" } } ] }
但解码时出现错误:
keyNotFound(CodingKeys(stringValue: "videoId", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "items", intValue: nil), _JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "id", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: "videoId", intValue: nil) ("videoId").", underlyingError: nil))
问题原因
错误提示明确指出,在第2个(索引1)items元素的id对象中找不到videoId字段。这是因为YouTube Search API默认会返回多种类型的结果,除了你需要的video类型,还可能包含channel(频道)或playlist(播放列表)类型。这些非视频类型的id对象中,对应的字段是channelId或playlistId,而非videoId,而你的模型IdVideoElement强制要求videoId为必填字段,因此解码非视频类型的结果时就会失败。
解决方法
方法1:API请求限制返回视频类型(推荐)
直接在API请求URL中添加type=video参数,让API只返回视频类型的结果,这样每个items元素的id对象都会包含videoId字段:
guard let url = URL(string: "\(Constants.Youtube_Base_URL)q=\(query)&key=\(Constants.Youtube_API_KEY)&type=video") else { return }
方法2:修改模型兼容多种ID类型
如果需要保留多种类型的结果,可以修改IdVideoElement,将不同类型的ID字段设为可选属性:
struct IdVideoElement: Codable { let kind: String let videoId: String? let channelId: String? let playlistId: String? }
之后在处理数据时,可以根据kind字段判断当前结果类型,再读取对应的ID:
for item in results.items { switch item.id.kind { case "youtube#video": if let videoId = item.id.videoId { // 处理视频ID } case "youtube#channel": if let channelId = item.id.channelId { // 处理频道ID } case "youtube#playlist": if let playlistId = item.id.playlistId { // 处理播放列表ID } default: break } }
方法3:使用枚举实现更严谨的类型区分
如果希望代码类型安全性更高,可以用枚举来封装不同类型的ID:
struct VideoElement: Codable { let kind: String let etag: String let id: IdElement } enum IdElement: Codable { case video(id: String) case channel(id: String) case playlist(id: String) private enum CodingKeys: String, CodingKey { case kind case videoId case channelId case playlistId } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let kind = try container.decode(String.self, forKey: .kind) switch kind { case "youtube#video": let videoId = try container.decode(String.self, forKey: .videoId) self = .video(id: videoId) case "youtube#channel": let channelId = try container.decode(String.self, forKey: .channelId) self = .channel(id: channelId) case "youtube#playlist": let playlistId = try container.decode(String.self, forKey: .playlistId) self = .playlist(id: playlistId) default: throw DecodingError.dataCorruptedError( forKey: .kind, in: container, debugDescription: "未知的ID类型: \(kind)" ) } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { case .video(let id): try container.encode("youtube#video", forKey: .kind) try container.encode(id, forKey: .videoId) case .channel(let id): try container.encode("youtube#channel", forKey: .kind) try container.encode(id, forKey: .channelId) case .playlist(let id): try container.encode("youtube#playlist", forKey: .kind) try container.encode(id, forKey: .playlistId) } } }
使用时可以通过枚举模式匹配处理不同类型:
for item in results.items { switch item.id { case .video(let videoId): // 处理视频ID case .channel(let channelId): // 处理频道ID case .playlist(let playlistId): // 处理播放列表ID } }
内容的提问来源于stack exchange,提问作者Mathew Es

