Correctly Parsing JSON in Swift 3
Solution 1:
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 var
iable 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
or123.0
– Swift:Int
orDouble
-
Bool - JSON:
true
orfalse
not in double quotes – Swift:true
orfalse
-
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 asURL
. - The
time
integer can be decoded asDate
with thedateDecodingStrategy
.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
Solution 2:
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>
Solution 3:
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)")
}
Solution 4:
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.
Solution 5:
Updated the isConnectToNetwork-Function
afterwards, thanks to this post.
I wrote an extra method for it:
import SystemConfiguration
func loadingJSON(_ link:String, postString:String, completionHandler: @escaping (_ JSONObject: AnyObject) -> ()) {
if(isConnectedToNetwork() == false){
completionHandler("-1" as AnyObject)
return
}
let request = NSMutableURLRequest(url: URL(string: link)!)
request.httpMethod = "POST"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request as URLRequest) { data, response, error in
guard error == nil && data != nil else { // check for fundamental networking error
print("error=\(error)")
return
}
if let httpStatus = response as? HTTPURLResponse , httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response)")
}
//JSON successfull
do {
let parseJSON = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
DispatchQueue.main.async(execute: {
completionHandler(parseJSON as AnyObject)
});
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
}
}
task.resume()
}
func isConnectedToNetwork() -> Bool {
var zeroAddress = sockaddr_in(sin_len: 0, sin_family: 0, sin_port: 0, sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
zeroAddress.sin_len = UInt8(MemoryLayout.size(ofValue: zeroAddress))
zeroAddress.sin_family = sa_family_t(AF_INET)
let defaultRouteReachability = withUnsafePointer(to: &zeroAddress) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {zeroSockAddress in
SCNetworkReachabilityCreateWithAddress(nil, zeroSockAddress)
}
}
var flags: SCNetworkReachabilityFlags = SCNetworkReachabilityFlags(rawValue: 0)
if SCNetworkReachabilityGetFlags(defaultRouteReachability!, &flags) == false {
return false
}
let isReachable = (flags.rawValue & UInt32(kSCNetworkFlagsReachable)) != 0
let needsConnection = (flags.rawValue & UInt32(kSCNetworkFlagsConnectionRequired)) != 0
let ret = (isReachable && !needsConnection)
return ret
}
So now you can easily call this in your app wherever you want
loadingJSON("yourDomain.com/login.php", postString:"email=\(userEmail!)&password=\(password!)") { parseJSON in
if(String(describing: parseJSON) == "-1"){
print("No Internet")
} else {
if let loginSuccessfull = parseJSON["loginSuccessfull"] as? Bool {
//... do stuff
}
}