Search code examples
jsonswiftdecodejsondecoder

Can't return or decode data with JSONDecoder


I am learning to decode data with JSON in an Xcode playground but can't figure out what's wrong with my code, I can't return data nor decode it . Here is my code:

import UIKit
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

extension URL {
    func withQueries(_ queries: [String: String]) -> URL? {
        var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
        components?.queryItems = queries.flatMap { URLQueryItem(name: $0.0, value: $0.1) }
        return components?.url
    }
}

struct StoreItems: Codable {
    let results: [StoreItem]
}

struct StoreItem: Codable {
    var name: String
    var artist: String
    var kind: String
    var artworkURL: URL
    var description: String

    enum CodingKeys: String, CodingKey {
        case name = "trackName"
        case artist = "artistName"
        case kind
        case artworkURL
        case description
    }

    enum AdditionalKeys: String, CodingKey {
        case longDescription
    }

    init(from decoder: Decoder) throws {
        let valueContainer = try decoder.container(keyedBy: CodingKeys.self)
        name = try valueContainer.decode(String.self, forKey: CodingKeys.name)
        artist = try valueContainer.decode(String.self, forKey: CodingKeys.artist)
        kind = try valueContainer.decode(String.self, forKey: CodingKeys.kind)
        artworkURL = try valueContainer.decode(URL.self, forKey: CodingKeys.artworkURL)

        if let description = try? valueContainer.decode(String.self, forKey: CodingKeys.description) {
            self.description = description
        } else {
            let additionalValues = try decoder.container(keyedBy: AdditionalKeys.self)
            description = (try? additionalValues.decode(String.self, forKey: AdditionalKeys.longDescription)) ?? ""
        }
    }
}

func fetchItems(matching query: [String: String], completion: @escaping ([StoreItem]?) -> Void) {

    let baseURL = URL(string: "https://www.itunes.apple.com/search?")!

    guard let url = baseURL.withQueries(query) else {
        completion(nil)
        print("Unable to build URL with supplied queries.")
        return
    }

    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        let decoder = JSONDecoder()
        if let data = data,
            let storeItems = try? decoder.decode(StoreItems.self, from: data) {
            completion(storeItems.results)
        } else {
            print("Either no data was returned or data was not properly decoded.")
            completion(nil)
            return
        }
    }
    task.resume()
}

let query: [String: String] = [
    "term": "Inside Out 2015",
    "media": "movie",
    "lang": "en_us",
    "limit": "10"
]

fetchItems(matching: query) { (items) in
    print(items)
}

And here is what's printed to the console which I guess shows that something is wrong with my "task":

Either no data was returned or data was not properly decoded.

nil


Solution

  • A couple of issues:

    1. The URL is wrong. There is no www.
    2. The artworkURL appears to be optional, as your search didn't return a value for that key.

    When I fixed those, it works:

    extension URL {
        func withQueries(_ queries: [String: String]) -> URL? {
            var components = URLComponents(url: self, resolvingAgainstBaseURL: true)
            components?.queryItems = queries.flatMap { URLQueryItem(name: $0.0, value: $0.1) }
            return components?.url
        }
    }
    
    struct StoreItems: Codable {
        let results: [StoreItem]
    }
    
    struct StoreItem: Codable {
        var name: String
        var artist: String
        var kind: String
        var artworkURL: URL?
        var shortDescription: String?
        var longDescription: String?
    
        enum CodingKeys: String, CodingKey {
            case name = "trackName"
            case artist = "artistName"
            case kind, artworkURL, shortDescription, longDescription
        }
    }
    
    enum FetchError: Error {
        case urlError
        case unknownNetworkError
    }
    
    func fetchItems(matching query: [String: String], completion: @escaping ([StoreItem]?, Error?) -> Void) {
    
        let baseURL = URL(string: "https://itunes.apple.com/search")!
    
        guard let url = baseURL.withQueries(query) else {
            completion(nil, FetchError.urlError)
            return
        }
    
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            guard let data = data, error == nil else {
                completion(nil, error ?? FetchError.unknownNetworkError)
                return
            }
    
            do {
                let storeItems = try JSONDecoder().decode(StoreItems.self, from: data)
                completion(storeItems.results, nil)
            } catch let parseError {
                completion(nil, parseError)
            }
        }
        task.resume()
    }
    

    And:

    let query = [
        "term": "Inside Out 2015",
        "media": "movie",
        "lang": "en_us",
        "limit": "10"
    ]
    
    fetchItems(matching: query) { items, error in
        guard let items = items, error == nil else {
            print(error ?? "Unknown error")
            return
        }
    
        print(items)
    }
    

    Note, I'd suggest you add the error to the completion handler so that you can see why it failed (in your case, the first issue was that the URL was wrong).