Hello All,
I am having trouble with decoding part of the JSON that I get back from the TMDB API. The part that I'm having trouble with is the release_dates
. It returns nil
from the JSON decoder and I dont know why:
Here is the URL
Im using: you'll need an API Key to run it.
https://api.themoviedb.org/3/movie/438148?api_key=**YOUR API KEY**&append_to_response=videos,credits,release_dates
Here is the JSON I get back if I put that URL
in a browser and run: I'm going to apologize for how long the JSON is. I did shorten it a bit as in took some elements of the arrays out. But I wanted you to see what I get back from the browser, which I will assume is the same thing my app is getting.
{
"adult": false,
"backdrop_path": "/nmGWzTLMXy9x7mKd8NKPLmHtWGa.jpg",
"belongs_to_collection":
{
"id": 544669,
"name": "Minions Collection",
"poster_path": "/AfYGGvHufd8cIosTvBtnzUExxe4.jpg",
"backdrop_path": "/62Qe28oi9PaK3P2ljDYUDTGAyST.jpg"
},
"budget": 85000000,
"genres":
[
{
"id": 10751,
"name": "Family"
},
{
"id": 16,
"name": "Animation"
}
],
"homepage": "https://www.minionsmovie.com/",
"id": 438148,
"imdb_id": "tt5113044",
"original_language": "en",
"original_title": "Minions: The Rise of Gru",
"overview": "A fanboy of a supervillain supergroup known as the Vicious 6, Gru hatches a plan to become evil enough to join them, with the backup of his followers, the Minions.",
"popularity": 11087.461,
"poster_path": "/wKiOkZTN9lUUUNZLmtnwubZYONg.jpg",
"production_companies":
[
{
"id": 33,
"logo_path": "/8lvHyhjr8oUKOOy2dKXoALWKdp0.png",
"name": "Universal Pictures",
"origin_country": "US"
},
{
"id": 6704,
"logo_path": "/acf3yqGq8Bm9smHwkQGRDVO5CM5.png",
"name": "Illumination Entertainment",
"origin_country": "US"
}
],
"production_countries":
[
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "2022-06-29",
"revenue": 399884000,
"runtime": 87,
"spoken_languages":
[
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
}
],
"status": "Released",
"tagline": "A villain will rise.",
"title": "Minions: The Rise of Gru",
"video": false,
"vote_average": 7.6,
"vote_count": 343,
"videos":
{
"results":
[
{
"iso_639_1": "en",
"iso_3166_1": "US",
"name": "Official Trailer 3",
"key": "HhIl_XJ-OGA",
"site": "YouTube",
"size": 1080,
"type": "Trailer",
"official": true,
"published_at": "2022-06-06T15:00:15.000Z",
"id": "629e8340caa50837e3b31cfd"
},
{
"iso_639_1": "en",
"iso_3166_1": "US",
"name": "Official Trailer 2",
"key": "6DxjJzmYsXo",
"site": "YouTube",
"size": 1080,
"type": "Trailer",
"official": true,
"published_at": "2022-03-30T15:00:03.000Z",
"id": "624487897caa4700476aac08"
}
]
},
"credits":
{
"cast":
[
{
"adult": false,
"gender": 2,
"id": 4495,
"known_for_department": "Acting",
"name": "Steve Carell",
"original_name": "Steve Carell",
"popularity": 39.287,
"profile_path": "/dzJtsLspH5Bf8Tvw7OQC47ETNfJ.jpg",
"cast_id": 6,
"character": "Gru (voice)",
"credit_id": "5dec1a93eee18600128b94f8",
"order": 0
},
{
"adult": false,
"gender": 2,
"id": 124747,
"known_for_department": "Acting",
"name": "Pierre Coffin",
"original_name": "Pierre Coffin",
"popularity": 11.324,
"profile_path": "/5HojYPP34c4agydy7D90apCMNHn.jpg",
"cast_id": 0,
"character": "Kevin / Stuart / Bob / Minions (voice)",
"credit_id": "588fdb0a92514133a50000f5",
"order": 1
}
],
"crew":
[
{
"adult": false,
"gender": 2,
"id": 5720,
"known_for_department": "Production",
"name": "Christopher Meledandri",
"original_name": "Christopher Meledandri",
"popularity": 5.409,
"profile_path": "/iRcvLhuIzkUkjCR4C3NKbi2juuv.jpg",
"credit_id": "5e3aea0443250f0015c74505",
"department": "Production",
"job": "Producer"
},
{
"adult": false,
"gender": 0,
"id": 7963,
"known_for_department": "Visual Effects",
"name": "Carter Goodrich",
"original_name": "Carter Goodrich",
"popularity": 1.575,
"profile_path": null,
"credit_id": "62bf906428723c004c809ff3",
"department": "Visual Effects",
"job": "Character Designer"
}
]
},
"release_dates":
{
"results":
[
{
"iso_3166_1": "BR",
"release_dates":
[
{
"certification": "L",
"iso_639_1": null,
"note": "",
"release_date": "2022-06-30T00:00:00.000Z",
"type": 3
}
]
},
{
"iso_3166_1": "NL",
"release_dates":
[
{
"certification": "",
"iso_639_1": "",
"note": "",
"release_date": "2022-06-29T00:00:00.000Z",
"type": 3
}
]
}
]
}
}
In my function that loads and decodes it I printed out what I am getting back:
Illumination", site: "YouTube")])), release_dates: nil)
Here is my struct that I'm using: again I apologize for the length, just want you to see everything
struct MovieResponse: Decodable {
let results: [TMDBMovie]
}
struct TMDBMovie: Decodable, Identifiable, Hashable {
static func == (lhs: TMDBMovie, rhs: TMDBMovie) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
var id: Int
let title: String
let backdropPath: String?
let posterPath: String?
let overview: String
let voteAverage: Double
let voteCount: Int
let runtime: Int?
let releaseDate: String?
let genres: [MovieGenre]?
let credits: MovieCredit?
let videos: MovieVideoResponse?
let release_dates: ReleaseDates?
static private let yearFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy"
return formatter
}()
static private let durationFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .full
formatter.allowedUnits = [.hour, .minute]
return formatter
}()
var backdropURL: URL {
return URL(string: "https://image.tmdb.org/t/p/w500\(backdropPath ?? "")")!
}
var posterURL: URL {
return URL(string: "https://image.tmdb.org/t/p/w500\(posterPath ?? "")")!
}
var genreText: String {
genres?.first?.name ?? "n/a"
}
var ratingText: String {
let rating = Int(voteAverage)
let ratingText = (0..<rating).reduce("") { (acc, _) -> String in
return acc + "★"
}
return ratingText
}
var scoreText: String {
guard ratingText.count > 0 else {
return "n/a"
}
return "\(ratingText.count)/10"
}
var yearText: String {
guard let releaseDate = self.releaseDate, let date = Utilities.dateFormater.date(from: releaseDate) else {
return "n/a"
}
return TMDBMovie.yearFormatter.string(from: date)
}
var durationText: String {
guard let runtime = self.runtime, runtime > 0 else {
return "n/a"
}
return TMDBMovie.durationFormatter.string(from: TimeInterval(runtime) * 60) ?? "n/a"
}
var cast: [MovieCast]? {
credits?.cast
}
var crew: [MovieCrew]? {
credits?.crew
}
var directors: [MovieCrew]? {
crew?.filter { $0.job.lowercased() == "director" }
}
var producers: [MovieCrew]? {
crew?.filter { $0.job.lowercased() == "producer" }
}
var screenWriters: [MovieCrew]? {
crew?.filter { $0.job.lowercased() == "story" }
}
var youtubeTrailers: [MovieVideo]? {
videos?.results.filter { $0.youtubeURL != nil }
}
var mpaaRating: String {
if let releaseDate = release_dates?.results.filter({ $0.iso_3166_1.uppercased() == "US" }) {
return releaseDate[0].release_dates[0].certification
}
return ""
}
}
// MARK: - Genre
struct MovieGenre: Decodable {
let name: String
}
// MARK: - Credits (Cast and Crew)
struct MovieCredit: Decodable {
let cast: [MovieCast]
let crew: [MovieCrew]
}
struct MovieCast: Decodable, Identifiable {
let id: Int
let character: String
let name: String
}
struct MovieCrew: Decodable, Identifiable {
let id: Int
let job: String
let name: String
}
// MARK: - YouTube Videos
struct MovieVideoResponse: Decodable {
let results: [MovieVideo]
}
struct MovieVideo: Decodable, Identifiable {
let id: String
let key: String
let name: String
let site: String
var youtubeURL: URL? {
guard site == "YouTube" else {
return nil
}
return URL(string: "https://youtube.com/watch?v=\(key)")
}
}
// MARK: - Release Dates
struct ReleaseDates: Decodable {
let results: [ReleaseDatesResult]
}
struct ReleaseDatesResult: Decodable {
let iso_3166_1: String
let release_dates: [ReleaseDate]
}
struct ReleaseDate: Decodable {
let certification: String
}
// Here is the function I use to load and decode
private func loadURLAndDecode<D: Decodable>(url: URL, params: [String: String]? = nil) async throws -> D {
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
throw MovieError.invalidEndpoint
}
var queryItems = [URLQueryItem(name: "api_key", value: apiKey)]
if let params = params {
queryItems.append(contentsOf: params.map { URLQueryItem(name: $0.key, value: $0.value) })
}
urlComponents.queryItems = queryItems
guard let finalURL = urlComponents.url else {
throw MovieError.invalidEndpoint
}
print("finalURL: \(finalURL)")
let (data, response) = try await urlSession.data(from: finalURL)
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
throw MovieError.invalidResponse
}
print("DATA: \(try self.jsonDecoder.decode(D.self, from: data))")
return try self.jsonDecoder.decode(D.self, from: data)
}
If anyone can help with this or point out what might be happening I'd appreciate it very much.
Thanks
Taz