Correctly Parsing JSON in Swift 3

前端 未结 9 964
甜味超标
甜味超标 2020-11-21 07:00

I\'m trying to fetch a JSON response and store the results in a variable. I\'ve had versions of this code work in previous releases of Swift, until the GM version of Xcode 8

相关标签:
9条回答
  • 2020-11-21 07:36

    A big change that happened with Xcode 8 Beta 6 for Swift 3 was that id now imports as Any rather than AnyObject.

    This means that parsedData is returned as a dictionary of most likely with the type [Any:Any]. Without using a debugger I could not tell you exactly what your cast to NSDictionary will do but the error you are seeing is because dict!["currently"]! has type Any

    So, how do you solve this? From the way you've referenced it, I assume dict!["currently"]! is a dictionary and so you have many options:

    First you could do something like this:

    let currentConditionsDictionary: [String: AnyObject] = dict!["currently"]! as! [String: AnyObject]  
    

    This will give you a dictionary object that you can then query for values and so you can get your temperature like this:

    let currentTemperatureF = currentConditionsDictionary["temperature"] as! Double
    

    Or if you would prefer you can do it in line:

    let currentTemperatureF = (dict!["currently"]! as! [String: AnyObject])["temperature"]! as! Double
    

    Hopefully this helps, I'm afraid I have not had time to write a sample app to test it.

    One final note: the easiest thing to do, might be to simply cast the JSON payload into [String: AnyObject] right at the start.

    let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments) as! Dictionary<String, AnyObject>
    
    0 讨论(0)
  • 2020-11-21 07:39

    Swift has a powerful type inference. Lets get rid of "if let" or "guard let" boilerplate and force unwraps using functional approach:

    1. Here is our JSON. We can use optional JSON or usual. I'm using optional in our example:
    
        let json: Dictionary<String, Any>? = ["current": ["temperature": 10]]
    
    
    1. Helper functions. We need to write them only once and then reuse with any dictionary:
    
        /// Curry
        public func curry<A, B, C>(_ f: @escaping (A, B) -> C) -> (A) -> (B) -> C {
            return { a in
                { f(a, $0) }
            }
        }
    
        /// Function that takes key and optional dictionary and returns optional value
        public func extract<Key, Value>(_ key: Key, _ json: Dictionary<Key, Any>?) -> Value? {
            return json.flatMap {
                cast($0[key])
            }
        }
    
        /// Function that takes key and return function that takes optional dictionary and returns optional value
        public func extract<Key, Value>(_ key: Key) -> (Dictionary<Key, Any>?) -> Value? {
            return curry(extract)(key)
        }
    
        /// Precedence group for our operator
        precedencegroup RightApplyPrecedence {
            associativity: right
            higherThan: AssignmentPrecedence
            lowerThan: TernaryPrecedence
        }
    
        /// Apply. g § f § a === g(f(a))
        infix operator § : RightApplyPrecedence
        public func §<A, B>(_ f: (A) -> B, _ a: A) -> B {
            return f(a)
        }
    
        /// Wrapper around operator "as".
        public func cast<A, B>(_ a: A) -> B? {
            return a as? B
        }
    
    
    1. And here is our magic - extract the value:
    
        let temperature = (extract("temperature") § extract("current") § json) ?? NSNotFound
    
    

    Just one line of code and no force unwraps or manual type casting. This code works in playground, so you can copy and check it. Here is an implementation on GitHub.

    0 讨论(0)
  • 2020-11-21 07:42

    The problem is with the API interaction method. The JSON parsing is changed only in syntax. The main problem is with the way of fetching data. What you are using is a synchronous way of getting data. This doesn't work in every case. What you should be using is an asynchronous way to fetch data. In this way, you have to request data through the API and wait for it to respond with data. You can achieve this with URL session and third party libraries like Alamofire. Below is the code for URL Session method.

    let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
    let url = URL.init(string: urlString)
    URLSession.shared.dataTask(with:url!) { (data, response, error) in
        guard error == nil else {
            print(error)
        }
        do {
            let Data = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
            // Note if your data is coming in Array you should be using [Any]()
            //Now your data is parsed in Data variable and you can use it normally
            let currentConditions = Data["currently"] as! [String:Any]
            print(currentConditions)
            let currentTemperatureF = currentConditions["temperature"] as! Double
            print(currentTemperatureF)
        } catch let error as NSError {
            print(error)
        }
    }.resume()
    
    0 讨论(0)
  • 2020-11-21 07:45

    I built quicktype exactly for this purpose. Just paste your sample JSON and quicktype generates this type hierarchy for your API data:

    struct Forecast {
        let hourly: Hourly
        let daily: Daily
        let currently: Currently
        let flags: Flags
        let longitude: Double
        let latitude: Double
        let offset: Int
        let timezone: String
    }
    
    struct Hourly {
        let icon: String
        let data: [Currently]
        let summary: String
    }
    
    struct Daily {
        let icon: String
        let data: [Datum]
        let summary: String
    }
    
    struct Datum {
        let precipIntensityMax: Double
        let apparentTemperatureMinTime: Int
        let apparentTemperatureLowTime: Int
        let apparentTemperatureHighTime: Int
        let apparentTemperatureHigh: Double
        let apparentTemperatureLow: Double
        let apparentTemperatureMaxTime: Int
        let apparentTemperatureMax: Double
        let apparentTemperatureMin: Double
        let icon: String
        let dewPoint: Double
        let cloudCover: Double
        let humidity: Double
        let ozone: Double
        let moonPhase: Double
        let precipIntensity: Double
        let temperatureHigh: Double
        let pressure: Double
        let precipProbability: Double
        let precipIntensityMaxTime: Int
        let precipType: String?
        let sunriseTime: Int
        let summary: String
        let sunsetTime: Int
        let temperatureMax: Double
        let time: Int
        let temperatureLow: Double
        let temperatureHighTime: Int
        let temperatureLowTime: Int
        let temperatureMin: Double
        let temperatureMaxTime: Int
        let temperatureMinTime: Int
        let uvIndexTime: Int
        let windGust: Double
        let uvIndex: Int
        let windBearing: Int
        let windGustTime: Int
        let windSpeed: Double
    }
    
    struct Currently {
        let precipProbability: Double
        let humidity: Double
        let cloudCover: Double
        let apparentTemperature: Double
        let dewPoint: Double
        let ozone: Double
        let icon: String
        let precipIntensity: Double
        let temperature: Double
        let pressure: Double
        let precipType: String?
        let summary: String
        let uvIndex: Int
        let windGust: Double
        let time: Int
        let windBearing: Int
        let windSpeed: Double
    }
    
    struct Flags {
        let sources: [String]
        let isdStations: [String]
        let units: String
    }
    

    It also generates dependency-free marshaling code to coax the return value of JSONSerialization.jsonObject into a Forecast, including a convenience constructor that takes a JSON string so you can quickly parse a strongly typed Forecast value and access its fields:

    let forecast = Forecast.from(json: jsonString)!
    print(forecast.daily.data[0].windGustTime)
    

    You can install quicktype from npm with npm i -g quicktype or use the web UI to get the complete generated code to paste into your playground.

    0 讨论(0)
  • 2020-11-21 07:52
    let str = "{\"names\": [\"Bob\", \"Tim\", \"Tina\"]}"
    
    let data = str.data(using: String.Encoding.utf8, allowLossyConversion: false)!
    
    do {
        let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: AnyObject]
        if let names = json["names"] as? [String] 
    {
            print(names)
    }
    } catch let error as NSError {
        print("Failed to load: \(error.localizedDescription)")
    }
    
    0 讨论(0)
  • 2020-11-21 07:53

    First of all never load data synchronously from a remote URL, use always asynchronous methods like URLSession.

    'Any' has no subscript members

    occurs because the compiler has no idea of what type the intermediate objects are (for example currently in ["currently"]!["temperature"]) and since you are using Foundation collection types like NSDictionary the compiler has no idea at all about the type.

    Additionally in Swift 3 it's required to inform the compiler about the type of all subscripted objects.

    You have to cast the result of the JSON serialization to the actual type.

    This code uses URLSession and exclusively Swift native types

    let urlString = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"
    
    let url = URL(string: urlString)
    URLSession.shared.dataTask(with:url!) { (data, response, error) in
      if error != nil {
        print(error)
      } else {
        do {
    
          let parsedData = try JSONSerialization.jsonObject(with: data!) as! [String:Any]
          let currentConditions = parsedData["currently"] as! [String:Any]
    
          print(currentConditions)
    
          let currentTemperatureF = currentConditions["temperature"] as! Double
          print(currentTemperatureF)
        } catch let error as NSError {
          print(error)
        }
      }
    
    }.resume()
    

    To print all key / value pairs of currentConditions you could write

     let currentConditions = parsedData["currently"] as! [String:Any]
    
      for (key, value) in currentConditions {
        print("\(key) - \(value) ")
      }
    

    A note regarding jsonObject(with data:

    Many (it seems all) tutorials suggest .mutableContainers or .mutableLeaves options which is completely nonsense in Swift. The two options are legacy Objective-C options to assign the result to NSMutable... objects. In Swift any variable is mutable by default and passing any of those options and assigning the result to a let constant has no effect at all. Further most of the implementations are never mutating the deserialized JSON anyway.

    The only (rare) option which is useful in Swift is .allowFragments which is required if if the JSON root object could be a value type(String, Number, Bool or null) rather than one of the collection types (array or dictionary). But normally omit the options parameter which means No options.

    ===========================================================================

    Some general considerations to parse JSON

    JSON is a well-arranged text format. It's very easy to read a JSON string. Read the string carefully. There are only six different types – two collection types and four value types.


    The collection types are

    • Array - JSON: objects in square brackets [] - Swift: [Any] but in most cases [[String:Any]]
    • Dictionary - JSON: objects in curly braces {} - Swift: [String:Any]

    The value types are

    • String - JSON: any value in double quotes "Foo", even "123"or "false" – Swift: String
    • Number - JSON: numeric values not in double quotes 123 or 123.0 – Swift: Int or Double
    • Bool - JSON: true or false not in double quotes – Swift: true or false
    • null - JSON: null – Swift: NSNull

    According to the JSON specification all keys in dictionaries are required to be String.


    Basically it's always recommeded to use optional bindings to unwrap optionals safely

    If the root object is a dictionary ({}) cast the type to [String:Any]

    if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [String:Any] { ...
    

    and retrieve values by keys with (OneOfSupportedJSONTypes is either JSON collection or value type as described above.)

    if let foo = parsedData["foo"] as? OneOfSupportedJSONTypes {
        print(foo)
    } 
    

    If the root object is an array ([]) cast the type to [[String:Any]]

    if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]] { ...
    

    and iterate through the array with

    for item in parsedData {
        print(item)
    }
    

    If you need an item at specific index check also if the index exists

    if let parsedData = try JSONSerialization.jsonObject(with: data!) as? [[String:Any]], parsedData.count > 2,
       let item = parsedData[2] as? OneOfSupportedJSONTypes {
          print(item)
        }
    }
    

    In the rare case that the JSON is simply one of the value types – rather than a collection type – you have to pass the .allowFragments option and cast the result to the appropriate value type for example

    if let parsedData = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? String { ...
    

    Apple has published a comprehensive article in the Swift Blog: Working with JSON in Swift


    ===========================================================================

    In Swift 4+ the Codable protocol provides a more convenient way to parse JSON directly into structs / classes.

    For example the given JSON sample in the question (slightly modified)

    let jsonString = """
    {"icon": "partly-cloudy-night", "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precip_intensity": 0, "wind_speed": 6.04, "summary": "Partly Cloudy", "ozone": 321.13, "temperature": 49.45, "dew_point": 41.75, "apparent_temperature": 47, "wind_bearing": 332, "cloud_cover": 0.28, "time": 1480846460}
    """
    

    can be decoded into the struct Weather. The Swift types are the same as described above. There are a few additional options:

    • Strings representing an URL can be decoded directly as URL.
    • The time integer can be decoded as Date with the dateDecodingStrategy .secondsSince1970.
    • snaked_cased JSON keys can be converted to camelCase with the keyDecodingStrategy .convertFromSnakeCase

    struct Weather: Decodable {
        let icon, summary: String
        let pressure: Double, humidity, windSpeed : Double
        let ozone, temperature, dewPoint, cloudCover: Double
        let precipProbability, precipIntensity, apparentTemperature, windBearing : Int
        let time: Date
    }
    
    let data = Data(jsonString.utf8)
    do {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .secondsSince1970
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        let result = try decoder.decode(Weather.self, from: data)
        print(result)
    } catch {
        print(error)
    }
    

    Other Codable sources:

    • Apple: Encoding and Decoding Custom Types
    • HackingWithSwift: Codable Cheat Sheet
    • Ray Wenderlich: Encoding and Decoding in Swift
    0 讨论(0)
提交回复
热议问题