A few months ago Apple introduced a new programming language, Swift, that left us excited about the future of iOS and OS X development. People were jumping into Swift with Xcode Beta1 immediately and it didn’t take long to realize that parsing JSON, something almost every app does, was not going to be as easy as in Objective-C. Swift being a statically typed language meant we could no longer haphazardly throw objects into typed variables and have the compiler trust us that it was actually the type we claimed it would be. Now, in Swift, the compiler is doing the checking, making sure we don’t accidentally cause runtime errors. This allows us to lean on the compiler to create bug free code, but means we have to do a bit more work to make it happy. In this post, I discuss a method of parsing JSON APIs that uses functional concepts and Generics to make readable and efficient code.
Request the User Model
The first thing we need is a way to parse the data we receive from a network
request into JSON. In the past, we’ve used
NSJSONSerialization.JSONObjectWithData(NSData, Int, &NSError)
which gives us
an optional JSON data type and a
possible error if there were problems with the parsing. The JSON object data type in Objective-C
is NSDictionary
which can hold any object in its values. With Swift, we have
a new dictionary type that requires us to specify the types held within. JSON objects now map to
Dictionary<String, AnyObject>
. AnyObject
is used because a JSON value could be a String
,
Double
, Bool
, Array
, Dictionary
or null
. When we try to use the JSON to populate a model we’ve
created, we’ll have to test that each key we get from the JSON dictionary is of that model’s
property type.
As an example, let’s look at a user model:
struct User {
let id: Int
let name: String
let email: String
}
Now let’s take a look at what a request and response for the current user might look like:
func getUser(request: NSURLRequest, callback: (User) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
var jsonErrorOptional: NSError?
let jsonOptional: AnyObject! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
if let json = jsonOptional as? Dictionary<String, AnyObject> {
if let id = json["id"] as AnyObject? as? Int { // Currently in beta 5 there is a bug that forces us to cast to AnyObject? first
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(user)
}
}
}
}
}
task.resume()
}
After a lot of if-let
statements, we finally have our User
object. You can
imagine that a model with more properties will just get uglier and uglier.
Also, we are not handling any errors so if any of the steps don’t succeed, we
have nothing. Finally, we would have to write this code for every model we want
from the API, which would be a lot of code duplication.
Before we start to refactor, let’s define some typealias
‘s to simplify the
JSON types.
typealias JSON = AnyObject
typealias JSONDictionary = Dictionary<String, JSON>
typealias JSONArray = Array<JSON>
Refactoring: Add Error Handling
First, we will refactor our function to handle errors by introducing the first
functional programming concept, the Either<A, B>
type. This will let us
return the user object when everything runs smoothly or an error when it
doesn’t. We can implement an Either<A, B>
type in Swift like this:
enum Either<A, B> {
case Left(A)
case Right(B)
}
We can use Either<NSError, User>
as the type we’ll pass to our callback so the
caller can handle the successfully parsed User
or the error.
func getUser(request: NSURLRequest, callback: (Either<NSError, User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
// if the response returned an error send it to the callback
if let err = error {
callback(.Left(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// if there was an error parsing the JSON send it back
if let err = jsonErrorOptional {
callback(.Left(err))
return
}
if let json = jsonOptional as? JSONDictionary {
if let id = json["id"] as AnyObject? as? Int {
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(.Right(user))
return
}
}
}
}
// if we couldn't parse all the properties then send back an error
callback(.Left(NSError()))
}
task.resume()
}
Now the function calling our getUser
can switch on the Either
and do
something with the user or display the error.
getUser(request) { either in
switch either {
case let .Left(error):
// display error message
case let .Right(user):
// do something with user
}
}
We will simplify this a bit by assuming that the Left
will always be an
NSError
. Instead let’s use a different type Result<A>
which will either
hold the value we are looking for or an error. It’s implementation might look
like this:
enum Result<A> {
case Error(NSError)
case Value(A)
}
In the current version of Swift (Beta 5), the above Result
type will cause
compiler errors. Swift needs to know what types we’ll be placing inside
the enum
’s cases. We can create a constant class to hold onto a generic
value for us.
final class Box<A> {
let value: A
init(_ value: A) {
self.value = value
}
}
enum Result<A> {
case Error(NSError)
case Value(Box<A>)
}
Replacing Either
with Result
will look like this:
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
// if the response returned an error send it to the callback
if let err = error {
callback(.Error(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// if there was an error parsing the JSON send it back
if let err = jsonErrorOptional {
callback(.Error(err))
return
}
if let json = jsonOptional as? JSONDictionary {
if let id = json["id"] as AnyObject? as? Int {
if let name = json["name"] as AnyObject? as? String {
if let email = json["email"] as AnyObject? as? String {
let user = User(id: id, name: name, email: email)
callback(.Value(Box(user)))
return
}
}
}
}
// if we couldn't parse all the properties then send back an error
callback(.Error(NSError()))
}
task.resume()
}
getUser(request) { result in
switch result {
case let .Error(error):
// display error message
case let .Value(boxedUser):
let user = boxedUser.value
// do something with user
}
}
Not a big change but let’s keep going.
Refactoring: Eliminate Type Checking Tree
Next, we will get rid of the ugly JSON parsing by creating separate JSON parsers for each type.
We only have a String
, Int
, and Dictionary
in our object so we need three
functions to parse those types.
func JSONString(object: JSON?) -> String? {
return object as? String
}
func JSONInt(object: JSON?) -> Int? {
return object as? Int
}
func JSONObject(object: JSON?) -> JSONDictionary? {
return object as? JSONDictionary
}
Now the JSON parsing will look like this:
if let json = JSONObject(jsonOptional) {
if let id = JSONInt(json["id"]) {
if let name = JSONString(json["name"]) {
if let email = JSONString(json["email"]) {
let user = User(id: id, name: name, email: email)
}
}
}
}
Using these functions we’ll still need a bunch of if-let
syntax. The
functional programming concepts Monads, Applicative Functors, and
Currying will help to condense this parsing. First, let’s look at the
Maybe Monad which is similar to Swift optionals. Monads have a bind operator
which, when used with optionals, allows us to bind an optional with a function
that takes a non-optional and returns an optional. If the first optional is
.None
then it returns .None
, otherwise it unwraps the first optional and
applies the function to it.
infix operator >>> { associativity left precedence 150 }
func >>><A, B>(a: A?, f: A -> B?) -> B? {
if let x = a {
return f(x)
} else {
return .None
}
}
In other functional languages, >>=
is used for bind; however, in Swift that
operator is used for bitshifting so we will use >>>
instead. Applying this
to the JSON parsing we get:
if let json = jsonOptional >>> JSONObject {
if let id = json["id"] >>> JSONInt {
if let name = json["name"] >>> JSONString {
if let email = json["email"] >>> JSONString {
let user = User(id: id, name: name, email: email)
}
}
}
}
Then we can remove the optional parameters from our parsers:
func JSONString(object: JSON) -> String? {
return object as? String
}
func JSONInt(object: JSON) -> Int? {
return object as? Int
}
func JSONObject(object: JSON) -> JSONDictionary? {
return object as? JSONDictionary
}
Functors have an fmap
operator for applying functions to values wrapped in
some context. Applicative Functors also have an apply
operator for applying
wrapped functions to values wrapped in some context. The context here is an
Optional
which wraps our value. This means that we can combine multiple
optional values with a function that takes multiple non-optional values. If
all values are present, .Some
, then we get a result wrapped in an optional.
If any of the values are .None
, we get .None
. We can define these
operators in Swift like this:
infix operator <^> { associativity left } // Functor's fmap (usually <$>)
infix operator <*> { associativity left } // Applicative's apply
func <^><A, B>(f: A -> B, a: A?) -> B? {
if let x = a {
return f(x)
} else {
return .None
}
}
func <*><A, B>(f: (A -> B)?, a: A?) -> B? {
if let x = a {
if let fx = f {
return fx(x)
}
}
return .None
}
Before we put it all together, we will need to manually curry our User
’s init
since Swift doesn’t support auto-currying. Currying means that if we give a
function fewer parameters than it takes, it will return a function that takes
the remaining parameters. Our User
model will now look like this:
struct User {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
}
Putting it all together, our JSON parsing now looks like this:
if let json = jsonOptional >>> JSONObject {
let user = User.create <^>
json["id"] >>> JSONInt <*>
json["name"] >>> JSONString <*>
json["email"] >>> JSONString
}
If any of our parser’s return .None
then user
will be .None
. This looks
much better, but we’re not done yet.
Now, our getUser
function looks like this:
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
// if the response returned an error send it to the callback
if let err = error {
callback(.Error(err))
return
}
var jsonErrorOptional: NSError?
let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
// if there was an error parsing the JSON send it back
if let err = jsonErrorOptional {
callback(.Error(err))
return
}
if let json = jsonOptional >>> JSONObject {
let user = User.create <^>
json["id"] >>> JSONInt <*>
json["name"] >>> JSONString <*>
json["email"] >>> JSONString
if let u = user {
callback(.Value(Box(u)))
return
}
}
// if we couldn't parse all the properties then send back an error
callback(.Error(NSError()))
}
task.resume()
}
Refactoring: Remove Multiple Returns with Bind
Notice that we’re calling callback
four times in the previous function. If we
were to forget one of the return statements, we could introduce a bug. We can
eliminate this potential bug and clean up this function further by first
breaking up this function into 3 distinct parts: parse the response, parse the
data into JSON, and parse the
JSON into our User
object.
Each of these steps takes one input and returns the next step’s input or an
error. This sounds like a perfect case for using bind with our Result
type.
The parseResponse
function will need a Result
with data
and the status
code of the response. The iOS API only gives us NSURLResponse
and keeps the data separate,
so we will make a small struct to help out here:
struct Response {
let data: NSData
let statusCode: Int = 500
init(data: NSData, urlResponse: NSURLResponse) {
self.data = data
if let httpResponse = urlResponse as? NSHTTPURLResponse {
statusCode = httpResponse.statusCode
}
}
}
Now we can pass our parseResponse
function a Response
and check the response
for errors before handing back the data.
func parseResponse(response: Response) -> Result<NSData> {
let successRange = 200..<300
if !contains(successRange, response.statusCode) {
return .Error(NSError()) // customize the error message to your liking
}
return .Value(Box(response.data))
}
The next functions will require us to transform an optional to a Result
type
so let’s make one quick abstraction before we move on.
func resultFromOptional<A>(optional: A?, error: NSError) -> Result<A> {
if let a = optional {
return .Value(Box(a))
} else {
return .Error(error)
}
}
Next up is our data to JSON function:
func decodeJSON(data: NSData) -> Result<JSON> {
let jsonOptional: JSON! = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: &jsonErrorOptional)
return resultFromOptional(jsonOptional, NSError()) // use the error from NSJSONSerialization or a custom error message
}
Then, we add our JSON to model decoding on the model itself:
struct User {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
static func decode(json: JSON) -> Result<User> {
let user = JSONObject(json) >>> { dict in
User.create <^>
dict["id"] >>> JSONInt <*>
dict["name"] >>> JSONString <*>
dict["email"] >>> JSONString
}
return resultFromOptional(user, NSError()) // custom error message
}
}
Before we combine it all, let’s extend bind, >>>
, to also work with the
Result
type:
func >>><A, B>(a: Result<A>, f: A -> Result<B>) -> Result<B> {
switch a {
case let .Value(x): return f(x.value)
case let .Error(error): return .Error(error)
}
}
And add a custom initializer to Result
:
enum Result<A> {
case Error(NSError)
case Value(Box<A>)
init(_ error: NSError?, _ value: A) {
if let err = error {
self = .Error(err)
} else {
self = .Value(Box(value))
}
}
}
Now, we combine all these functions with the bind operator.
func getUser(request: NSURLRequest, callback: (Result<User>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
let result = responseResult >>> parseResponse
>>> decodeJSON
>>> User.decode
callback(result)
}
task.resume()
}
Wow, even writing this again, I’m excited with this result. You might think, “This is really cool. Can’t wait to use it!”, but we’re not done yet!
Refactoring: Type Agnostic using Generics
This is great but we still have to write this for every model we want to get. We can use Generics to make this completely abstracted.
We introduce a JSONDecodable
protocol and tell our function that the type we
want back must conform to that protocol. The protocol looks like this:
protocol JSONDecodable {
class func decode(json: JSON) -> Self?
}
Next, we write a function that will decode any model that conforms to
JSONDecodable
into a Result
:
func decodeObject<A: JSONDecodable>(json: JSON) -> Result<A> {
return resultFromOptional(A.decode(json), NSError()) // custom error
}
Now make User
conform:
struct User: JSONDecodable {
let id: Int
let name: String
let email: String
static func create(id: Int)(name: String)(email: String) -> User {
return User(id: id, name: name, email: email)
}
static func decode(json: JSON) -> User? {
return JSONObject(json) >>> { d in
User.create <^>
d["id"] >>> JSONInt <*>
d["name"] >>> JSONString <*>
d["email"] >>> JSONString
}
}
We changed our User
decoder function to return an optional User
instead of
a Result<User>
. This allows us to have an abstracted function that calls
resultFromOptional
after a decode instead of having to call that in every
model decode
function.
Finally, we will abstract the parsing and decoding from the performRequest
function for readability. We now have our final performRequest
and
parseResult
functions:
func performRequest<A: JSONDecodable>(request: NSURLRequest, callback: (Result<A>) -> ()) {
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, urlResponse, error in
callback(parseResult(data, urlResponse, error))
}
task.resume()
}
func parseResult<A: JSONDecodable>(data: NSData!, urlResponse: NSURLResponse!, error: NSError!) -> Result<A> {
let responseResult = Result(error, Response(data: data, urlResponse: urlResponse))
return responseResult >>> parseResponse
>>> decodeJSON
>>> decodeObject
}
Further Learning
The example code is available on GitHub.
If you are curious about functional programming or any of the concepts discussed in this post, check out Haskell and specifically this post from the Learn You a Haskell book. Also, check out Pat Brisbin’s post about options parsing using the Applicative.