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.

\$\endgroup\$
2

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)
}
\$\endgroup\$
0

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.