Swift JSON Redux - Part 1

Episode #130 | 30 minutes | published on July 31, 2014
Subscribers Only
In this episode we attempt to write a more idiomatic JSON Parsing framework leveraging Swift. To accomplish this we'll lean heavily on Swift's powerful enum features and apply a couple of custom operators to clean up syntax and reduce redundant code.

Note on Beta Software

This episode covers Swift as of Xcode 6 Beta 4. There may be breaking changes in future versions of Xcode. If you do notice a change in a future version, please leave a comment and I'll note corrections here.

As Xcode 6 is still in NDA, this episode is actually written in VIM and compilation is done on the command line.

Episode Links

  • Source Code
  • Swiftz Library - This episode was heavily inspired by Swiftz's JSON parsing code. There are some interesting tidbits in this library, so check it out.

Overview

The idea of this example is to parse the JSON structure into a nested type that we can work with. The core of this idea is to capture every possible value into an enum case that holds on to the captured value. Each value is stored as a JSValue enum. Arrays and inner objects can be composed of other JSValues as well.

Core JSValue Enum

enum JSValue {
  case JSArray([JSValue])
  case JSObject( [ String : JSValue ] )
  case JSNumber(Double)
  case JSString(String)
  case JSBool(Bool)
  case JSNull()
}

Note that for each case (except null) we have a typed value associated with the enum. When we go to extract values from this JSValue enum we'll already have the type.

Building the JSON Structure

We'll start with a function to convert from an NSData to our root element as a JSValue:

static func decode(data: NSData) -> JSValue? {
    var error: NSError?
    let options: NSJSONReadingOptions = .AllowFragments
    let object: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &error)

    if let json: AnyObject = object {
      return make(json as NSObject)
    } else {
      println("Couldn't parse the json provided")
      return nil
    }
  }

Here we call a make function which does the heavy lifting for us. It turns out that this function is also called recursively.

static func make(obj: NSObject) -> JSValue {
    switch obj {
      case let s as NSString: return .JSString(s)
      case let n as NSNumber: return .JSNumber(n)
      case let null as NSNull: return .JSNull()
      case let a as NSArray: return makeArray(a)
      case let d as NSDictionary: return makeObject(d)
      default:
        println("Unhandled type: <\(obj)>")
        abort()
    }
  }

This uses swift's pattern matching to detect the type of underlying value in the parsed JSON document in order to build up our JSValue instances. For NSArray and NSDictionary the parsing is a bit more involved, so these are extracted into their own functions:

static func makeArray(array: NSArray) -> JSValue {
    var items = Array<JSValue>()
    for obj in array {
      let jsValue = make(obj as NSObject)
      items.append(jsValue)
    }

    return .JSArray(items)
  }

  static func makeObject(dict: NSDictionary) -> JSValue {
    var object = Dictionary<String, JSValue>()
    for (key, value) in dict {
      object[key as String] = make(value as NSObject)
    }
    return .JSObject(object)
  }

So we have JSValues, now what?

Now we need to be able to pull out a value from our JSValue structure as a typed value, or nil if the type didn't match. We can lean on a protocol to define the contract for us:

protocol JSONDecode {
  typealias J
  class func fromJSON(j: JSValue) -> J?
}

Implementers will need to define what type J is.

class JSInt: JSONDecode {
  typealias J = Int
  class func fromJSON(j: JSValue) -> Int? {
    switch j {
      case let .JSNumber(n):
        return Int(n)
      default:
        return nil
    }
  }
}

class JSString: JSONDecode {
  typealias J = String
  class func fromJSON(j: JSValue) -> String? {
    switch j {
      case let .JSString(s): return s
      default: return nil
    }
  }
}

We'd need a decoder class for each type that we want to pull out of a JSValue.

Some helpful operators

When pulling a value out of our JSON dictionary, we might get nil. We can't call fromJSON with a nil value (at least not as it stands now) so we can use a custom "bind" operator to make this quick nil check for us:

func >>=<A, B>(source: A?, f: A -> B?) -> B? {
  if source {
    return f(source!)
  } else {
    return nil
  }
}

We also want a function to nil-coalesce values for us so we can provide default values.

operator infix ||= { precedence 10 } // very low precedence will be evaluated last

func ||=<T>(value: T?, defaultValue: T) -> T {
  if let actualValue = value {
    return actualValue
  }
  return defaultValue
}

Putting it together

With these in place we can extract values like this:

NSData *jsonData = // some data retrieved from an API
let json = JSValue.decode(jsonData)
switch json {
  case let .JSObject(d):
    return Episode(
      id:     d["id"] >>= JSInt.fromJSON ||= 0,
      title:  d["title"] >>= JSString.fromJSON ||= "(untitled)">
    )

  default:
    return nil
}
blog comments powered by Disqus