KingTim
KingTim

Reputation: 1301

Serializing JSON in Swift 4 - problem figuring out data type

I'm grabbing some JSON from an online API and putting the results into arrays for future use. All of the data so far has been fine (just string arrays) but I can't figure out how to work with one of the results.

This is the JSON (someone advised that I use https://jsonlint.com to make it readable and it's super helpful)

This is the function that gets the JSON:

func getJSON(completionHandler: @escaping (Bool) -> ()) {
    let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"
    guard let url = URL(string: jsonUrlString) else {return}

    URLSession.shared.dataTask(with: url) { (data, response, err) in
        guard let data = data, err == nil else {
            print(err!)
            return
        }

        do {
            let response = try
                JSONDecoder().decode(TopStoriesResponse.self, from: data)

            // Pass results into arrays (title, abstract, url, image)
            for result in response.results {
                let headlines = result.title
                let abstracts = result.abstract
                let url = result.url

                self.headlines.append(headlines)
                self.abstracts.append(abstracts)
                self.urls.append(url)
            }

            let imageResponse = try
                JSONDecoder().decode(Story.self, from: data)
            for imageResults in imageResponse.multimedia {
                let images = imageResults.url
                self.images.append(images)
            }

            completionHandler(true)


        } catch let jsonErr {
            print("Error serializing JSON", jsonErr)
        }
    }.resume()
}

These are the structs for serializing the JSON:

struct TopStoriesResponse: Decodable {
    let status: String
    let results: [Story]
}

struct Story: Decodable {
    let title: String
    let abstract: String
    let url: String
    let multimedia: [Multimedia]
}

struct Multimedia: Codable {
    let url: String
    let type: String
}

And I'm organizing the results into these arrays:

var headlines = [String]()
var abstracts = [String]()
var urls = [String]()
var images = [String]()

And I call the function in viewDidLoad

getJSON { (true) in
    print("Success")
    print("\n\nHeadlines: \(self.headlines)\n\nAbstracts: \(self.abstracts)\n\nURLS: \(self.urls)\n\nImages: \(self.images)")
}

As you can see in the getJSON function, I attempt to get the images using

let imageResponse = try JSONDecoder().decode(Story.self, from: data)
for imageResults in imageResponse.multimedia {
    let images = imageResults.url
    self.images.append(images)
}

But I get the error

CodingKeys(stringValue: "multimedia", intValue: nil)], debugDescription: "Expected to decode Array but found a string/data instead.", underlyingError: nil))

I'm confused because it's saying that it's expecting an array but found a string instead - are the images not an array, just like the headlines, abstracts etc?

Upvotes: 0

Views: 321

Answers (3)

Suhit Patil
Suhit Patil

Reputation: 12023

The problem is with the JSON response coming from the server, the Multimedia array is coming as empty "" String in one of the JSON response. To handle such a case you need to manually implement init(from decoder:) method and handle the empty String case. Also you don't need to create separate arrays to store the values, you can directly pass the TopStoriesResponse struct in your completion handler closure and get the values in your ViewController when needed.

Let's say you create an enum of Result which has success(T) and failure(Error) and you pass it in your completionHandler for ViewController to handle

enum Result<T> {
    case success(T)
    case failure(Error)
}

struct Networking {

    static func getJson(completionHandler: @escaping (Result<TopStoriesResponse>) -> ()) {

        let jsonUrlString = "https://api.nytimes.com/svc/topstories/v1/business.json?api-key=f4bf2ee721031a344b84b0449cfdb589:1:73741808"

        guard let url = URL(string: jsonUrlString) else {
            return
        }

        URLSession.shared.dataTask(with: url) { (data, response, error) in

            guard let data = data, error == nil else {
                completionHandler(Result.failure(error!))
                return
            }

            do {
                let topStoriesResponse: TopStoriesResponse = try JSONDecoder().decode(TopStoriesResponse.self, from: data)
                print(topStoriesResponse.results.count)
                completionHandler(Result.success(topStoriesResponse))
            } catch {
                completionHandler(Result.failure(error))
            }
        }.resume()
    }
}

Now in your ViewController you can call the getJson method and switch over result enum in completionHandler to get the values

class ViewController: UIViewController {

    var topStories: TopStoriesResponse?

    override func viewDidLoad() {
        super.viewDidLoad()
        loadData()
    }

    func loadData() {
        Networking.getJson { (result: Result<TopStoriesResponse>) in
            switch result {
            case let .success(topStories):
                self.topStories = topStories

                topStories.results.forEach({ (story: Story) in
                    print("Title: \(story.title) \n Abstracts = \(story.abstract)  URL = \(story.url)")
                })
                //reload tableView
            case let .failure(error):
                print(error.localizedDescription)
            }
        }
    }
}

To handle empty String case, you need to implement init(decoder:) method in your Multimedia struct as explained above

struct Multimedia: Decodable {
    let url: String
    let image: String
    let height: Float
    let width: Float
}

struct Story: Decodable {
    let title: String
    let abstract: String
    let url: String
    let multimedia: [Multimedia]

    private enum CodingKeys: String, CodingKey {
        case title
        case abstract
        case url
        case multimedia
    }

    init(from decoder:Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        abstract = try container.decode(String.self, forKey: .abstract)
        url = try container.decode(String.self, forKey: .url)
        multimedia = (try? container.decode([Multimedia].self, forKey: .multimedia)) ?? []
    }
}

struct TopStoriesResponse: Decodable {
    let status: String
    let copyright: String
    let num_results: Int
    let results: [Story]
}

Upvotes: 0

David Pasztor
David Pasztor

Reputation: 54785

The issue is that multimedia is either an array of Multimedia objects or an empty String. You need to write a custom initialiser for Story to handle that.

struct Story: Decodable {
    let title: String
    let abstract: String
    let url: String
    let multimedia: [Multimedia]

    private enum CodingKeys: String, CodingKey {
        case title
        case abstract
        case url
        case multimedia
    }

    init(from decoder:Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        abstract = try container.decode(String.self, forKey: .abstract)
        url = try container.decode(String.self, forKey: .url)
        multimedia = (try? container.decode([Multimedia].self, forKey: .multimedia)) ?? []
    }
}

Upvotes: 1

Kon
Kon

Reputation: 4099

You have multimedia defined as an Array. There are sections in that json that don't have any multimedia, which is set to an empty string:

multimedia: ""

You need to be able to handle either case. Since Codable is designed to deal with concrete types, you may be better off using JSONSerialization instead.

If you have a strong preference for using Codable, you can manipulate the the JSON response in string form to convert multimedia: "" into the format you expect, then pass it to decoder. For example, you can make multimedia optional and simply remove any lines with multimedia: "".

Upvotes: 0

Related Questions