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
}
我正在尝试用 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
}