Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
98 views
in Technique[技术] by (71.8m points)

Correctly Parsing JSON in Swift 3

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 was released. I had a look at a few similar posts on StackOverflow: Swift 2 Parsing JSON - Cannot subscript a value of type 'AnyObject' and JSON Parsing in Swift 3.

However, it seems the ideas conveyed there do not apply in this scenario.

How do I correctly parse the JSON reponse in Swift 3? Has something changed in the way JSON is read in Swift 3?

Below is the code in question (it can be run in a playground):

import Cocoa

let url = "https://api.forecast.io/forecast/apiKey/37.5673776,122.048951"

if let url = NSURL(string: url) {
    if let data = try? Data(contentsOf: url as URL) {
        do {
            let parsedData = try JSONSerialization.jsonObject(with: data as Data, options: .allowFragments)

        //Store response in NSDictionary for easy access
        let dict = parsedData as? NSDictionary

        let currentConditions = "(dict!["currently"]!)"

        //This produces an error, Type 'Any' has no subscript members
        let currentTemperatureF = ("(dict!["currently"]!["temperature"]!!)" as NSString).doubleValue

            //Display all current conditions from API
            print(currentConditions)

            //Output the current temperature in Fahrenheit
            print(currentTemperatureF)

        }
        //else throw an error detailing what went wrong
        catch let error as NSError {
            print("Details of JSON parsing error:
 (error)")
        }
    }
}

Edit: Here is a sample of the results from the API call after print(currentConditions)

["icon": partly-cloudy-night, "precipProbability": 0, "pressure": 1015.39, "humidity": 0.75, "precipIntensity": 0, "windSpeed": 6.04, "summary": Partly Cloudy, "ozone": 321.13, "temperature": 49.45, "dewPoint": 41.75, "apparentTemperature": 47, "windBearing": 332, "cloudCover": 0.28, "time": 1480846460]
Question&Answers:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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:


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...