Decoding JSON Error: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil

Decoding JSON Error: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil

我正在尝试用 SwiftUI/Combine 解析一些 JSON 数据,我对收到的错误感到有点困惑。我真的是 Combine 的新手,所以我可能完全忽略了一些东西。我确信这与真正的问题无关,因为如果我使用 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, tagline, 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",
        "origin_country": "US"
    },
    {
        "id": 20555,
        "logo_path": "/hD8yEGUBlHOcfHYbujp71vD8gZp.png",
        "name": "Taurus Film",
        "origin_country": "DE"
    },
    {
        "id": 54051,
        "logo_path": null,
        "name": "Atman Entertainment",
        "origin_country": ""
    },
    {
        "id": 54052,
        "logo_path": null,
        "name": "Knickerbocker Films",
        "origin_country": "US"
    },
    {
        "id": 25,
        "logo_path": "/qZCc1lty5FzX30aOCVRBLzaVmcp.png",
        "name": "20th Century Fox",
        "origin_country": "US"
    },
    {
        "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 等中设置 most/all var...可选,添加“?”。 在我的测试中效果很好。

编辑:

这是我用来测试答案的代码。适合我:

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, tagline, 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密钥(或任何其他密钥)!

第二个:

您正在调用的端点似乎是 returning 单个 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]
}

处理单个和多个对象结果

有时您不知道服务器是否会 return 单个或多个对象(并且您无法控制服务器来修复该问题)。 您可以实现自定义解码器来处理单个和多个对象响应:

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
}