Could not cast value of type 'NSNull' to 'NSString' and then the app crashes

488 Views Asked by At

We are trying to make a function to get JSON from an API..We know that this is giving us NIL but we dont know why the error is occuring. The exact error message that we got was

[] 2020-08-01 16:29:26.501199-0400 HEFT[97766:2952325] [] nw_proxy_resolver_create_parsed_array [C1 proxy pac] Evaluation error: NSURLErrorDomain: -1003 Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8). 2020-08-01 16:29:26.670549-0400 HEFT[97766:2952139] Could not cast value of type 'NSNull' (0x7fff87a92380) to 'NSString' (0x7fff87b502e8). (lldb)

We have tried messing around the code to find a solution and we tried to use some other questions but none of them were related with what we were trying to achieve.

func getJson() {
        if let url = URL(string: "https://api.weather.gov/alerts/active?area=GA") {
            URLSession.shared.dataTask(with: url) { (data:Data?, response:URLResponse?, error:Error?) in
                if error == nil {
                    if data != nil {
                        if let json = try? JSONSerialization.jsonObject(with: data!, options: []) as? [String:AnyObject] {
                            DispatchQueue.main.async {
                                
                                //if let rawfeatures = json["features"] {
                                var rawfeatures = json["features"] as! [Dictionary< String, AnyObject>]
                                var keepgoingfeatures = rawfeatures.count
                                var FeatureIndex = 0
                                while keepgoingfeatures != 0{
                                    let currentRawFeature = rawfeatures[FeatureIndex]
                                    let currentRawFeatureProperties = currentRawFeature["properties"]
                                    let currentFeature = Feature()
                                    currentFeature.event = currentRawFeatureProperties!["event"] as! String
                                    currentFeature.description = currentRawFeatureProperties!["description"] as! String
                                    currentFeature.instructions = currentRawFeatureProperties!["instruction"] as! String
                                    currentFeature.urgency = currentRawFeatureProperties!["urgency"] as! String
                                    keepgoingfeatures -= 1
                                    FeatureIndex += 1
                                }
                            }
                        }
                    }
                    
                    
                } else {
                    print("We have an error")
                }
            }.resume()
        }
    }
2

There are 2 best solutions below

1
On

Some of these alerts have null for instructions. I’d suggest defining your object to acknowledge that this field is optional, i.e. that it might not be present. E.g.

struct Feature {
    let event: String
    let description: String
    let instruction: String?
    let urgency: String
}

And, when parsing it, I might suggest getting rid of all of those forced unwrapping operators, e.g.

enum NetworkError: Error {
    case unknownError(Data?, URLResponse?)
    case invalidURL
}

@discardableResult
func getWeather(area: String, completion: @escaping (Result<[Feature], Error>) -> Void) -> URLSessionTask? {
    // prepare request

    var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
    components.queryItems = [URLQueryItem(name: "area", value: area)]
    var request = URLRequest(url: components.url!)
    request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")

    // perform request

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard
            error == nil,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            let responseData = data,
            let responseDictionary = try? JSONSerialization.jsonObject(with: responseData) as? [String: Any],
            let rawFeatures = responseDictionary["features"] as? [[String: Any]]
        else {
            DispatchQueue.main.async {
                completion(.failure(error ?? NetworkError.unknownError(data, response)))
            }
            return
        }

        let features = rawFeatures.compactMap { feature -> Feature? in
            guard
                let properties = feature["properties"] as? [String: Any],
                let event = properties["event"] as? String,
                let description = properties["description"] as? String,
                let urgency = properties["urgency"] as? String
            else {
                print("required string absent!")
                return nil
            }
            let instruction = properties["instruction"] as? String

            return Feature(event: event, description: description, instruction: instruction, urgency: urgency)
        }

        DispatchQueue.main.async {
            completion(.success(features))
        }
    }

    task.resume()

    return task
}

A few other observations:

  1. I’ve removed all of the forced casting (the as!). You don’t want your app crashing if there was some problem in the server. For example, not infrequently I receive a 503 error. You don’t want to crash if the server is temporarily unavailable.

  2. The docs say that you should set the User-Agent, so I’m doing that above. Obviously, set the domain and email string constants accordingly.

  3. While you can build the URL manually, it’s safest to use URLComponents, as that will take care of any percent escaping that might be needed. It’s not needed here, but will be a useful pattern if you start to get into more complicated requests (e.g. need to specify a city name that has a space in it, such as “Los Angeles”).

  4. I’d suggest the above completion handler pattern so that the caller can know when the request is done. So you might do something like:

     getWeather(area: "GA") { result in
         switch result {
         case .failure(let error):
             print(error)
             // update UI accordingly
    
         case .success(let features):
             self.features = features        // update your model object
             self.tableView.reloadData()     // update your UI (e.g. I'm assuming a table view, but do whatever is appropriate for your app
         }
     }
    
  5. I’m returning the URLSessionTask in case you might want to cancel the request (e.g. the user dismisses the view in question), but I’ve marked it as a @discardableResult, so you don’t have to use that if you don’t want.

  6. I’ve replaced the tower of if statements with a guard statement. It makes the code a little easier to follow and adopts an “early exit” pattern, where you can more easily tie the exit code with the failure (if any).


Personally, I’d suggest that you take this a step further and get out of manually parsing JSONSerialization results. It’s much easier to let JSONDecoder do all of that for you. For example:

struct ResponseObject: Decodable {
    let features: [Feature]
}

struct Feature: Decodable {
    let properties: FeatureProperties
}

struct FeatureProperties: Decodable {
    let event: String?
    let description: String
    let instruction: String?
    let urgency: String
}

enum NetworkError: Error {
    case unknownError(Data?, URLResponse?)
    case invalidURL
}

@discardableResult
func getWeather(area: String, completion: @escaping (Result<[FeatureProperties], Error>) -> Void) -> URLSessionTask? {
    var components = URLComponents(string: "https://api.weather.gov/alerts/active")!
    components.queryItems = [URLQueryItem(name: "area", value: area)]
    var request = URLRequest(url: components.url!)
    request.setValue("(\(domain), \(email))", forHTTPHeaderField: "User-Agent")

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard
            error == nil,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            let responseData = data
        else {
            DispatchQueue.main.async {
                completion(.failure(error ?? NetworkError.unknownError(data, response)))
            }
            return
        }

        do {
            let responseObject = try JSONDecoder().decode(ResponseObject.self, from: responseData)
            DispatchQueue.main.async {
                completion(.success(responseObject.features.map { $0.properties }))
            }
        } catch let parseError {
            DispatchQueue.main.async {
                completion(.failure(parseError))
            }
        }
    }

    task.resume()

    return task
}
3
On

The short answer is because you force cast everything and assume a very specific format which the json doesnt have.

so at some point you read a value that just insnt there. Concretely instruction.

as a working/non crashing fix (which I locally ran!):

let currentFeature = Feature()
currentFeature.event = currentRawFeatureProperties!["event"] as? String ?? ""
currentFeature.description = currentRawFeatureProperties!["description"] as? String ?? ""
currentFeature.instructions = currentRawFeatureProperties!["instruction"]  as? String ?? ""
currentFeature.urgency = currentRawFeatureProperties!["urgency"] as? String ?? ""

I'd urge you to refactor your function broadly