Reputation: 1301
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
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
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
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