3
\$\begingroup\$

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.

nhgrif
25.4k3 gold badges64 silver badges129 bronze badges
asked Aug 24, 2015 at 21:24
\$\endgroup\$
2
  • \$\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\$ Commented 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\$ Commented Aug 24, 2015 at 23:01

1 Answer 1

4
\$\begingroup\$

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 an AnyObject?. It makes no assumptions about the type, since curr_dict is an NSDictionary. It either returns whatever value is at that key, or nil if the key doesn't exist.

  • as? String tries to cast that value as a String. If the value was nil or if it can't be cast as a string, it returns nil, otherwise it returns String.

  • let summary = declares the variable called summary, whose type is determined by the return of everything to the right. Since the top of the right-side's stack is as? String, which can return nil or a String, then the return type is String?.

  • if let casts an optional type (String?) as a non-optional type (String) if it can, otherwise it enters the else 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)
}
answered Aug 25, 2015 at 1:17
\$\endgroup\$
0

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.