-1

For several hours I've been trying to parse to a JSON file with an array to experiment with API's, but I keep getting errors. I managed to parse a file with just one JSON object/dictionary (following different tutorials), but I can't figure it out for a JSON file with an array of objects. I'm testing this out with a coronavirus API. Here is the URL so you can see the JSON code: https://coronavirus-19-api.herokuapp.com/countries. I feel like the solution is very simple and there's just something small that I'm missing, but I'm not really sure.

This is the error I get:

valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 10", intValue: 10), CodingKeys(stringValue: "recovered", intValue: nil)], debugDescription: "Expected Int value but found null instead.", underlyingError: nil))

Here is my code:

import UIKit

class FirstViewController: UIViewController {
    
    var coronaInfo = [CoronaInfo]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let url = "https://coronavirus-19-api.herokuapp.com/countries"
        
        getData(from: url) { (data) in
            self.coronaInfo = data
            print(self.coronaInfo[0].cases)   // <-- Works in here
        }
        
        print(coronaInfo[0].cases) // <-- Outside of the closure it's nil
    }
    
    func getData(from url: String, completed: @escaping ([CoronaInfo]) -> ()) {
        URLSession.shared.dataTask(with: URL(string: url)!) { data, response, error in
            // Make sure that data isn't nil and that there was no error
            guard let data = data, error == nil else { print("Something went wrong"); return }
            
            var result: [CoronaInfo]?
            do {
                // Convert data from bytes to the custom object (Known as JSON decoding)
                result = try JSONDecoder().decode([CoronaInfo].self, from: data)
                
                guard let json = result else { return }
                
                DispatchQueue.main.async {
                    completed(json)
                }
            } catch { print(error) }
            
        }.resume()
    }

}

struct CoronaInfo: Codable {
    let country: String
    let cases: Int
    let todayCases: Int
    let deaths: Int
    let todayDeaths: Int
    let recovered: Int?
    let active: Int?
    let critical: Int
    let casesPerOneMillion: Int
    let deathsPerOneMillion: Int
    let totalTests: Int
    let testsPerOneMillion: Int
}

Thanks in advance for any help!

Zemelware
  • 93
  • 8
  • The URL in she code and the URL in the text are different. The JSON of the former is not an array. – vadian Aug 12 '20 at 04:50
  • @vadian well I feel like a complete idiot, I forgot to change the URL! But I changed it and it didn't fix the issue. (I will update the URL in the code and the error message) – Zemelware Aug 12 '20 at 05:00
  • The `url` that you have mentioned here returns an `Object` and it is completely different from the `model` you have created. – caldera.sac Aug 12 '20 at 05:00

1 Answers1

0

Two points worth mentioning here

  1. The response you are trying to parse should come from https://coronavirus-19-api.herokuapp.com/countries not from https://corona-virus-stats.herokuapp.com/api/v1/cases/general-stats. So use first link instead of second.
        let url = "https://coronavirus-19-api.herokuapp.com/countries"
        getData(from: url)
  1. Since there are two fields with null values, mark them optional in your model. enter image description here
struct CoronaData: Codable {
    let country: String
    let cases: Int
    let todayCases: Int
    let deaths: Int
    let todayDeaths: Int
    let recovered: Int?
    let active: Int?
    let critical: Int
    let casesPerOneMillion: Int
    let deathsPerOneMillion: Int
    let totalTests: Int
    let testsPerOneMillion: Int
}
Sauvik Dolui
  • 5,520
  • 3
  • 34
  • 44
  • I really appreciate the answer, that fixed it! But I'm confused why those two properties need to be optionals. They are given a value in the JSON file, so why would they need to be optionals and not anything else? I am quite new with optionals, so sorry if this sounds very obvious. – Zemelware Aug 12 '20 at 05:12
  • `why those two properties need to be optionals?` Since incoming json response does to confirm to always give you a value for `recovered` and `active`, we should mark them as optionals, else error is thrown. `valueNotFound(Swift.Int, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 10", intValue: 10), CodingKeys(stringValue: "recovered", intValue: nil)], debugDescription: "Expected Int value but found null instead.", underlyingError: nil))` i.e. index 10, value for `recovered` field is `nil`, while it was expecting an `Int`. – Sauvik Dolui Aug 12 '20 at 05:31
  • Okay that makes more sense. By any chance do you know how to return the json constant? I can't have a return statement inside the closure, but if I put it outside then I can't reference the json constant and return it at all. – Zemelware Aug 12 '20 at 05:44
  • I think you might be looking for something like a Completion Closure. – Tagnal Aug 12 '20 at 06:30
  • This is an async call, ideally you should pass one completion block to `func getData(from url: String)`, to handle the result/error. This answer might be helpful https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function#answer-54597264. – Sauvik Dolui Aug 12 '20 at 06:36
  • @SauvikDolui so I implemented the async call, but the issue I'm having is that I can access my data in the closure that's passed as a parameter into the function, but not outside of the closure. To see what I mean, look at my code as I've updated it and it'll make more sense. Thank you. – Zemelware Aug 12 '20 at 15:36
  • `print(coronaInfo[0].cases) // <-- Outside of the closure it's nil`, it's ovious that here the data will be `nil`, this statement is executed even before your api call returns data or the clouser is executed. If you want to access the data from API, always try to execute your code (Table/Collection View Update OR simply setting incoming data in UI layer) inside the passed clouser. – Sauvik Dolui Aug 12 '20 at 15:51
  • @SauvikDolui I'm a little confused. I'm accessing the data after the getData() function call so shouldn't I be able to access the data since it's after the API call returns the data? What if I want to use the data outside of the closure? – Zemelware Aug 12 '20 at 21:21