SwiftUI JSON解析报错:期望数组却得到字典问题求助
问题解决及优化方案
核心错误分析
- 类型不匹配错误:你调用的TMDB
/movie/550接口返回的是单个电影对象(JSON字典),但代码里尝试解码成[Movie].self(数组类型),导致类型不匹配。 - 字段映射错误:TMDB返回的日期字段是
release_date,但结构体里写的是releaseDate,未做蛇形命名到驼峰的映射,解码失败。 - URL缺失API密钥:代码里的
urlString未包含api_key参数,实际请求会返回401错误,导致数据格式不正确。
修正后的代码
MoviesFetcher.swift
import Foundation struct Movie: Codable { let originalTitle: String let releaseDate: String // 手动指定字段映射,或使用下方keyDecodingStrategy二选一 enum CodingKeys: String, CodingKey { case originalTitle = "original_title" case releaseDate = "release_date" } } class MovieFetcher { // API密钥建议存入Info.plist,避免硬编码 private let apiKey = "d2ea758c1787f6f8715df6713125a180" func fetchSingleMovie(completion: @escaping (Result<Movie, Error>) -> Void) { let urlString = "https://api.themoviedb.org/3/movie/550?api_key=\(apiKey)" guard let url = URL(string: urlString) else { completion(.failure(NSError(domain: "InvalidURL", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"]))) return } let task = URLSession.shared.dataTask(with: url) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "NoData", code: -2, userInfo: [NSLocalizedDescriptionKey: "未获取到数据"]))) return } do { let decoder = JSONDecoder() // 若不用CodingKeys,可启用此策略自动转换蛇形到驼峰命名 // decoder.keyDecodingStrategy = .convertFromSnakeCase let movie = try decoder.decode(Movie.self, from: data) completion(.success(movie)) } catch { completion(.failure(error)) } } task.resume() } }
ContentView.swift
import SwiftUI struct ContentView: View { @State private var movie: Movie? @State private var errorMessage: String? var body: some View { VStack(spacing: 20) { if let errorMessage = errorMessage { Text("出错了:\(errorMessage)") .foregroundColor(.red) } else if let movie = movie { VStack(alignment: .leading) { Text("电影标题:\(movie.originalTitle)") .font(.title) Text("上映日期:\(movie.releaseDate)") .font(.subheadline) } } else { ProgressView("加载中...") } } .padding() .onAppear { MovieFetcher().fetchSingleMovie { result in DispatchQueue.main.async { switch result { case .success(let fetchedMovie): self.movie = fetchedMovie case .failure(let error): self.errorMessage = error.localizedDescription } } } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
优化建议
- 抛弃全局变量:原代码的
movieArray是全局变量,多线程环境下易出现数据竞争,改用completion闭包返回结果更安全。 - 用Result类型处理状态:通过
Result<Movie, Error>明确区分成功/失败状态,错误处理更清晰。 - 强制主线程更新UI:网络请求在后台线程执行,更新UI必须切回主线程,代码中用
DispatchQueue.main.async保证操作安全。 - 安全管理API密钥:不要硬编码密钥,建议添加到
Info.plist,通过Bundle.main.object(forInfoDictionaryKey: "TMDB_API_KEY")读取。 - 列表需求适配:若要获取电影数组,需调用TMDB列表接口(如
/movie/popular),此时解码成[Movie].self才正确,同步修改Fetcher方法和ContentView状态即可。
内容的提问来源于stack exchange,提问作者Gonçalo Ramos
相关产品推荐
相关产品推荐

