Swift JSON Redux Part 2

Episode #131 | 10 minutes | published on August 7, 2014 | Uses swift-1.0, ios-8.0-beta-4
Subscribers Only
In this episode we wrap up our JSON parsing exploration in Swift by extending the decoding to work with arrays. Doing so cleans up the extraction code significantly.

Note On Beta Software

As Xcode 6 is still under NDA I'm writing Swift in Vim. Swift's syntax may change in future releases, so I'll keep the associated code up to date.

This episode was recorded with iOS 8 SDK Beta 4. Beta 5 introduced some breaking changes, so check out the source from Github for a working example.

Beta 5 changes:

  • Swift now has a built-in null-coalescing operator (??), but unfortunately the precedence is higher than the >>= operator I was overloading. Since that operator is meant for bit-shifting, I have moved to using >>> instead so I can control the precedence.
  • You can no longer check for nil with simply if obj { .. }, instead, like if obj != nil { .. }

Episode Links

  • Episode Source Code
  • Swiftz - A more full featured library including many more features beyond JSON parsing. The JSON series was heavily inspired by Swiftz's JSON parsing.

Adding Support to Decode Arrays

class JSArray<A, B : JSONDecode where B.J == A> : JSONDecode {
  typealias J = [A]
  class func fromJSON(j: JSValue) -> [A]? {
    switch j {
      case let .JSArray(array):
        let mapped = array.map { B.fromJSON($0) }
        let results = compact(mapped)
        if results.count == mapped.count {
          return results
        } else {
          return nil
        }

      default: return nil
    }
  }
}

Note the use of the generic type parameters. You can think of this like A as the type we are encoding to and B being a JSONDecode object that outputs A.

We also bail return nil if any of the elements can't be parsed.

This code makes use of the compact function, which is defined like this:

func compact<T>(collection: [T?]) -> [T] {
  return filter(collection) {
    if $0 != nil {
      return true
    } else {
      return false
    }
  }.map { $0! }
}

This function takes a list of optional values, filters out anything that is nil, then unwraps each value, returning a new array of non-optional values.

Making Episode More Flexible

Right now we have to deal with an episode key in between our array and our dictionary in the JSON response. A simple way to get around this is to allow Episode.fromJSON work with either type of dictionary:

  static func fromJSON(j: JSValue) -> Episode? {
    switch j {
      case let .JSObject(d):

        if let innerEpisode = d["episode"] {
          return fromJSON(innerEpisode)
        }

        let id = d["id"] >>> JSInt.fromJSON ?? 0
        let title = d["title"] >>> JSString.fromJSON ?? "<untitled>"
        let descr = d["description"] >>> JSString.fromJSON ?? ""
        let number = d["episode_number"] >>> JSInt.fromJSON ?? 0
        return Episode(id: id,
            title: title,
            episodeDescription: descr,
            number: number)


      default:
        println("Expected a dictionary")
        return nil
    }
  }

With that in place we can consider our response as an array of episodes without having a random "container" object in our way.

Putting it Together

Now we can clean up our extraction code to simply:

downloader.downloadJson {
  (let data) in

  let json = JSValue.decode(data)
  if let episodes = json >>> JSArray<Episode, Episode>.fromJSON {
    for episode in episodes {
      println("Episode: \(episode.title)")
    }
  }
}
blog comments powered by Disqus