I am looking for the best way of parsing JSON in Swift 2.0.
I have this JSON
{ "latitude": 37.8267, "longitude": -122.423, "currently": { "time": 1440110395, "summary": "Clear" } }
and field summary
is NOT always visible in response.
My model object and parsing method:
enum JSONErrorType: ErrorType {
case InvalidJson
}
struct WeatherForecast {
let date: NSDate
let summary: String?
let latitude: Double
let longitude: Double
init(date: NSDate, summary: String?, latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
self.date = date
self.summary = summary
}
static func fromJSON(json: AnyObject) throws -> WeatherForecast {
guard let dict = json as? NSDictionary,
let latitude = dict["latitude"] as? Double,
let longitude = dict["longitude"] as? Double,
let curr_dict = dict["currently"] as? NSDictionary,
let timestamp = curr_dict["time"] as? NSTimeInterval else {
throw JSONErrorType.InvalidJson
}
let date = NSDate(timeIntervalSince1970: timestamp)
if let summary = curr_dict["summary"] as? String {
return WeatherForecast(date: date, summary: summary, latitude: latitude, longitude: longitude)
} else {
return WeatherForecast(date: date, summary: nil, latitude: latitude, longitude: longitude)
}
}
}
I can't use guard
because the summary
property is optional. if let
works here but I have two returns which doesn't look very good. Also if I will have more optionals fields, the code gets messy.
-
\$\begingroup\$ @Malachi Why did you edit the title? It's JSON parsing in swift 2.0 and it has nothing to do with weather API. I could use any other API in this example. \$\endgroup\$tailec– tailec2015年08月24日 22:46:18 +00:00Commented Aug 24, 2015 at 22:46
-
2\$\begingroup\$ As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does. For examples of good titles, check out Best of Code Review 2014 - Best Question Title Category You may also want to read How to get the best value out of Code Review - Asking Questions. \$\endgroup\$Malachi– Malachi2015年08月24日 23:01:38 +00:00Commented Aug 24, 2015 at 23:01
1 Answer 1
Remember optionals.
Let's start with answering your primary concern: what do I do with the summary
field?
You've already appropriately set up a constructor which takes a String?
argument for summary
. But your particular code never takes advantage of this fact, does it?
if let summary = curr_dict["summary"] as? String
Let's break down what's going on here piece by piece.
curr_dict["summary"]
returns anAnyObject?
. It makes no assumptions about the type, sincecurr_dict
is anNSDictionary
. It either returns whatever value is at that key, ornil
if the key doesn't exist.as? String
tries to cast that value as aString
. If the value wasnil
or if it can't be cast as a string, it returnsnil
, otherwise it returnsString
.let summary =
declares the variable calledsummary
, whose type is determined by the return of everything to the right. Since the top of the right-side's stack isas? String
, which can returnnil
or aString
, then the return type isString?
.if let
casts an optional type (String?
) as a non-optional type (String
) if it can, otherwise it enters theelse
block.
So... we can get rid of the if let
part, and just make ourselves an optional string, right?
let summary = curr_dict["summary"] as? String
return WeatherForecast(date: date, summary: summary, latitude: latitude, longitude: longitude)
But wait, there's more!
guard let dict = json as? NSDictionary, // ...
We should prefer to use Swift types over Objective-C types when that makes sense. And here is a place where it does. What we want is a [String:Any]
.
guard let dictionary = json as? [String:Any], // ...
Encapsulate.
We should also prefer encapsulation. It doesn't make particular sense to have a latitude
and longitude
property. What we need is a type that encapsulates these, as they're just two parts of describing one thing: location.
The easiest solution would be simply importing CoreLocation
and either using CLLocationCooridinate2D
or CLLocation
. The downside to this, however, is that any class that uses our struct has to import CoreLocation
in order to use this property, but we can easily get around this with a typealias
:
typealias WeatherForecastLocation = CLLocationCoordinate2D
And now we replace our latitude
and longitude
properties with a property of type WeatherForecastLocation
.
Informative Errors
So, JSONErrorType.InvalidJSON
... how useful is this? It's an enum with a single case, just so we have something to throw. We can be so much more informative.
First of all, two types of valid top-level JSON objects. Dictionary or array. So any method we write to parse JSON will expect one of these two types, so the first error will be the JSON not being that type. That gives use two errors right off the bat (and let's grab some better names here):
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
}
If we are looking for JSON with a top-level object of dictionary, and we don't find it, we throw ExpectedDictionaryNotFound
. It doesn't matter what it looked like. But this is already more informative... we couldn't even manage to get past the first level.
Now, what's another sort of JSON parsing error? Missing keys. But we don't want to simply be that generic. Let's specify what key is missing, shall we? Swift enums can have associated values. With our MissingKey
error, we should use a String
associated value to identify the missing key. Or enum
now looks like this:
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
case MissingKey(String)
}
We also have an error case for when the type of the value doesn't match what we expect. For this, we want to specify the key that's problematic and specify the type that we expect it to be, so let's associate a tuple of two strings:
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound
case ExpectedDictionaryNotFound
case MissingKey(String)
case InvalidTypeForKey(String,String)
}
So, the first string specifies the key, and the second string is a message something like "Expected Int"
, "Expected Array"
.
Now... the last thing we can do here that's probably quite helpful... return the JSON that was passed in with error.
enum JSONParseError: ErrorType {
case ExpectedArrayNotFound(Any)
case ExpectedDictionaryNotFound(Any)
case MissingKey(String,Any)
case InvalidTypeForKey(String,String,Any)
}