UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: JSON Issue

Forums > Swift

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

2      

Without know how is your JSONDecoder setup, it is hard to diagnose.

Are you using the setting to tell it to convert from snake_case automatically? Some attributes in your Swift models are written using camelCase but you also have snake_case in there. I would start there and go with camelCase for all attributes.

And make sure to set keyDecodingStrategy on your decoder to .convertFromSnakeCase

2      

Hello All,

I figured out my issue, I was using the keyDecodingStrategy = .convertFromSnakeCase and in my TMDBMovie struct i had the following:

let release_dates: ReleaseDates?

struct ReleaseDates: Decodable {
    let results: [ReleaseDatesResult]
}

struct ReleaseDatesResult: Decodable {
    let iso_3166_1: String
    let release_dates: [ReleaseDate]
}

struct ReleaseDate: Decodable {
    let certification: String
}

I just needed to change the snake case to camel case and it all worked. I don't know why forgot that, but I did. I figured instead of deleting this post I'd just add what I was missing and maybe it will help someone else.

Thanks to all who have read this long post. It's a fine line on how much you put in the post as far as code goes, but as I said I just wanted to give as much as possible.

Thanks, Taz

2      

@nemecek-filip

Thanks for the reply, I think I was posting at the same time you were. I changed the solved over to you since you came to the same conclusion that I did. It looks like we were both on the same track.

I figured it out by playing around in a playground. The issue was because I was using two different styles for declaring my properties. I thought that you had to declare them just as they were in the incoming JSON, but after reading about convertFromSnakeCase I realized that was doing it for me automatically. The decoding strategy was declared on the decoder itself.

Anyway, just wanted to say thanks for the reply, Taz

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.