You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift调用YouTube API时JSON解码报错:videoId字段缺失

YouTube API JSON解码keyNotFound问题排查与解决

问题描述

作为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 08:05:29