如何解决解码 JSON 错误:“预期解码 Array<Any> 但找到了字典”,underlyingError:nil 处理单对象和多对象结果
我正在尝试使用 SwiftUI/Combine 解析一些 JSON 数据,但我对遇到的错误有些困惑。我对结合真的很陌生,所以我可能会完全忽略一些东西。我确定这与真正的问题无关,因为如果我使用 urlsession/@escaping 以正常方式解析,这可能会发生。
代码如下:
struct FilmModel: Identifiable,Codable {
let adult: Bool
let backdropPath: String
let budget: Int
let genres: [Genre]
let homepage: String
let id: Int
let imdbID,originalLanguage,originalTitle,overview: String
let popularity: Double
let posterPath: String
let productionCompanies: [ProductionCompany]
let productionCountries: [ProductionCountry]
let releaseDate: String
let revenue,runtime: Int
let spokenLanguages: [SpokenLanguage]
let status,tagline,title: String
let video: Bool
let VoteAverage: Double
let VoteCount: Int
enum CodingKeys: String,CodingKey {
case adult
case backdropPath = "backdrop_path"
case budget
case genres
case homepage
case id
case imdbID = "imbd_id"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case overview
case popularity
case posterPath = "poster_path"
case productionCompanies = "production_companies"
case productionCountries = "production_countries"
case releaseDate = "release_date"
case revenue
case runtime
case spokenLanguages = "spoken_languages"
case status,title
case video
case VoteAverage = "Vote_average"
case VoteCount = "Vote_count"
}
struct Genre: Identifiable,Codable {
let id: Int
let name: String
}
struct ProductionCompany: Codable {
let id: Int
let logoPath: String?
let name,originCountry: String
}
struct ProductionCountry: Codable {
let iso3166_1,name: String
}
struct SpokenLanguage: Codable {
let englishName,iso639_1,name: String
}
JSON 响应:
{
"adult": false,"backdrop_path": "/rr7E0NoGKxvbkb89eR1GwfoYjpA.jpg","belongs_to_collection": null,"budget": 63000000,"genres": [
{
"id": 18,"name": "Drama"
}
],"homepage": "http://www.foxmovies.com/movies/fight-club","id": 550,"imdb_id": "tt0137523","original_language": "en","original_title": "fight Club","overview": "A ticking-time-bomb insomniac and a slippery soap salesman channel primal male aggression into a shocking new form of therapy. Their concept catches on,with underground \"fight clubs\" forming in every town,until an eccentric gets in the way and ignites an out-of-control spiral toward oblivion.","popularity": 46.456,"poster_path": "/pB8BM7pdSp6B6Ih7QZ4DrQ3PmJK.jpg","production_companies": [
{
"id": 508,"logo_path": "/7PzJdsLglr7oW4J0J5Xcd0pHGRg.png","name": "Regency Enterprises","origin_country": "US"
},{
"id": 711,"logo_path": "/tEiIH5QesdheJmDAqQwvtN60727.png","name": "Fox 2000 Pictures",{
"id": 20555,"logo_path": "/hD8yEGUBlHOcfHYbujp71vD8gZp.png","name": "Taurus Film","origin_country": "DE"
},{
"id": 54051,"logo_path": null,"name": "Atman Entertainment","origin_country": ""
},{
"id": 54052,"name": "Knickerbocker Films",{
"id": 25,"logo_path": "/qZCc1lty5FzX30aOCVRBLzaVmcp.png","name": "20th Century Fox",{
"id": 4700,"logo_path": "/A32wmjrs9Psf4zw0uaixF0GXfxq.png","name": "The Linson Company","origin_country": "US"
}
],"production_countries": [
{
"iso_3166_1": "DE","name": "Germany"
},{
"iso_3166_1": "US","name": "United States of America"
}
],"release_date": "1999-10-15","revenue": 100853753,"runtime": 139,"spoken_languages": [
{
"english_name": "English","iso_639_1": "en","name": "English"
}
],"status": "Released","tagline": "Mischief. Mayhem. Soap.","title": "fight Club","video": false,"Vote_average": 8.4,"Vote_count": 22054
数据服务:
class FilmDataService {
@Published var films: [FilmModel] = []
var filmSubscription: AnyCancellable?
init() {
getFilms()
}
private func getFilms() {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/550?api_key=<key>") else { return }
filmSubscription = URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: dispatchQueue.global(qos: .default))
.tryMap { (output) -> Data in
guard let response = output.response as? HTTPURLResponse,response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
.decode(type: [FilmModel].self,decoder: JSONDecoder())
.receive(on: dispatchQueue.main)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { [weak self] (returnedFilms) in
self?.films = returnedFilms
self?.filmSubscription?.cancel()
}
}
查看模型:
class Filmviewmodel: ObservableObject {
@Published var tabBarImageNames = ["house","rectangle.stack","clock.arrow.circlepath","square.and.arrow.down"]
@Published var films: [FilmModel] = []
private let dataService = FilmDataService()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscribers()
}
func addSubscribers() {
dataService.$films
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
.store(in: &cancellables)
}
解决方法
我的观察。您的错误可能与Combine 无关。
您正在尝试解码“[FilmModel].self”,但响应仅针对一部电影 FilmModel.self。
此外,我还会在您的 FilmModel 等中设置大部分/所有变量...可选,添加“?”。 它在我的测试中运行良好。
编辑:
这是我用来测试我的答案的代码。对我来说效果很好:
import Foundation
import SwiftUI
import Combine
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
@StateObject var movies = FilmViewModel()
var body: some View {
VStack (spacing: 50) {
Text("movie test")
ForEach(movies.films,id: \.id) { movie in
Text(movie.title ?? "no title").foregroundColor(.red)
}
}
}
}
class FilmDataService {
@Published var films: [FilmModel] = []
var filmSubscription: AnyCancellable?
init() {
getFilms()
}
private func getFilms() {
guard let url = URL(string: "https://api.themoviedb.org/3/movie/550?api_key=1f632307cea6ce33f288f9a232b9803b") else { return }
filmSubscription = URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .default))
.tryMap { (output) -> Data in
guard let response = output.response as? HTTPURLResponse,response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
.decode(type: FilmModel.self,decoder: JSONDecoder()) // <--- here
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .finished:
break
case .failure(let error):
print(error)
}
} receiveValue: { [weak self] (returnedFilms) in
self?.films.append(returnedFilms) // <--- here
self?.filmSubscription?.cancel()
}
}
}
class FilmViewModel: ObservableObject {
@Published var tabBarImageNames = ["house","rectangle.stack","clock.arrow.circlepath","square.and.arrow.down"]
@Published var films: [FilmModel] = []
private let dataService = FilmDataService()
private var cancellables = Set<AnyCancellable>()
init() {
addSubscribers()
}
func addSubscribers() {
dataService.$films
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
.store(in: &cancellables)
}
}
struct FilmModel: Identifiable,Codable {
let adult: Bool?
let backdropPath: String?
let budget: Int?
let genres: [Genre]?
let homepage: String?
let id: Int
let imdbID,originalLanguage,originalTitle,overview: String?
let popularity: Double?
let posterPath: String?
let productionCompanies: [ProductionCompany]?
let productionCountries: [ProductionCountry]?
let releaseDate: String?
let revenue,runtime: Int?
let spokenLanguages: [SpokenLanguage]?
let status,tagline,title: String?
let video: Bool?
let voteAverage: Double?
let voteCount: Int?
enum CodingKeys: String,CodingKey {
case adult
case backdropPath = "backdrop_path"
case budget
case genres
case homepage
case id
case imdbID = "imbd_id"
case originalLanguage = "original_language"
case originalTitle = "original_title"
case overview
case popularity
case posterPath = "poster_path"
case productionCompanies = "production_companies"
case productionCountries = "production_countries"
case releaseDate = "release_date"
case revenue
case runtime
case spokenLanguages = "spoken_languages"
case status,title
case video
case voteAverage = "vote_average"
case voteCount = "vote_count"
}
}
struct Genre: Identifiable,Codable {
let id: Int
let name: String?
}
struct ProductionCompany: Codable {
let id: Int
let logoPath: String?
let name,originCountry: String?
}
struct ProductionCountry: Codable {
let iso3166_1,name: String?
}
struct SpokenLanguage: Codable {
let englishName,iso639_1,name: String?
}
,
首先,切勿在线编写您的api 密钥(或任何其他密钥)!
其次:
您正在调用的端点似乎正在返回单个 FilmModel
。因此,您必须将其解码为单个:
改变这个:
.decode(type: [FilmModel].self,decoder: JSONDecoder())
为此:
.decode(type: FilmModel.self,decoder: JSONDecoder())
然后改变这个:
.sink { [weak self] (returnedFilms) in
self?.films = returnedFilms
}
到:
.sink { [weak self] (returnedFilm) in
self?.films = [returnedFilm]
}
处理单对象和多对象结果
有时您不知道服务器是否会返回单个或多个对象(并且您无法控制服务器来修复该问题)。 您可以实现自定义解码器来处理单对象和多对象响应:
enum FilmsResult {
case single(FilmModel)
case array([FilmModel])
}
extension FilmsResult: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let singleFilm = try? container.decode(FilmModel.self) {
self = .single(singleFilm)
} else {
try self = .array(container.decode([FilmModel].self))
}
}
}
extension FilmsResult {
var values: [FilmModel] {
switch self {
case .single(let film): return [film]
case .array(let films): return films
}
}
}
您可以将结果解码为:
.decode(type: FilmsResult.self,decoder: JSONDecoder())
并像这样使用它:
.sink { [weak self] filmsResult in
self?.films = filmsResult.values
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。