Skip to content

JaviSoto/Argo

 
 

Repository files navigation

Carthage compatible

Argo

The Greek word for swift and the ship used by Jason, son of Aeson, of the Argonauts. Aeson is the JSON parsing library in Haskell that inspired Argo, much like Aeson inspired his son Jason.

NOTE: The master branch of Argo is pushing ahead with Swift 1.2 support. Support for Swift 1.1 can be found on branch swift-1.1 and in the 0.3.x versions of tags/releases.

Installation

Add the following to your Cartfile:

github "thoughtbot/Argo"

Then run carthage update.

Follow the current instructions in Carthage's README for up to date installation instructions.

You'll also need to add Runes.framework to your project. Runes is a dependency of Argo, so you don't need to specify it in your Cartfile.

Add the following to your Podfile:

pod 'Argo'

You will also need to make sure you're opting into using frameworks:

use_frameworks!

Then run pod install with CocoaPods 0.36 or newer.

Git Submodules

I guess you could do it this way if that's your thing.

Add this repo as a submodule, and add the project file to your workspace. You can then link against Argo.framework for your application target.

You'll also need to add Runes to your project the same way.

Usage tl;dr:

struct User {
  let id: Int
  let name: String
  let email: String?
  let role: Role
  let companyName: String
  let friends: [User]
}

extension User: JSONDecodable {
  static func create(id: Int)(name: String)(email: String?)(role: Role)(companyName: String)(friends: [User]) -> User {
    return User(id: id, name: name, email: email, role: role, companyName: companyName, friends: friends)
  }

  static func decode(j: JSONValue) -> User? {
    return User.create
      <^> j <| "id"
      <*> j <| "name"
      <*> j <|? "email" // Use ? for parsing optional values
      <*> j <| "role" // Custom types that also conform to JSONDecodable just work
      <*> j <| ["company", "name"] // Parse nested objects
      <*> j <|| "friends" // parse arrays of objects
  }
}

// Wherever you receive JSON data:

let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: .None)

if let j: AnyObject = json {
  let value = JSONValue.parse(j)
  let user = User.decode(value)
}

Ideology

Argo's core concept is that in order to maintain type safety, you should only be able to successfully decode an object if all parameters are satisfied properly. So if you have a model that looks like this:

struct User {
  let id: Int
  let name: String
}

but the JSON you receive from the server looks like this:

{
  "user": {
    "id": "this isn't a number",
    "name": "Gob Bluth"
  }
}

you would want that JSON parsing to fail and return .None instead of a User object. That base concept means that when you're dealing with an actual User object, you can be sure that when you ask for user.id, you're going to get the type you expect.

If you're interested in learning more about the concepts and ideology that went into building Argo, we recommend reading the series of articles that were written alongside its development:

Functional Concepts

Argo really wants to be used with patterns borrowed from functional programming such as map and apply. We feel that these patterns greatly reduce the pain felt in trying to use JSON (an inherently loosely typed format) with Swift (a strictly typed language). It also gives us a way to succinctly maintain the core concept described above, and short circuit the decoding process if any part of it fails.

Additionally, we feel that the use of operators for these functions greatly improves the readability of the code we're suggesting. Using named functions would lead to nested functions and a confusing number of parenthesis.

If you aren't familiar with how these functions work (or just aren't comfortable with using operators), that's totally OK. It's possible to use the library without using them, although it might be a little more painful.

If you're looking to learn more about these functions, we would recommend reading the following articles:

Usage

The first thing you need to do when you receive JSON data is convert it to an instance of the JSONValue enum. This is done by passing the AnyObject value returned from NSJSONSerialization to JSONValue.parse():

let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions(0), error: nil)

if let j: AnyObject = json {
  let value: JSONValue = JSONValue.parse(j)
  let user: User? = User.decode(value)
}

Note that you probably want to use an error pointer to track errors from NSJSONSerialization.

The JSONValue enum exists to help with some of the type inference, and also wraps up some of the casting that we'll need to to to transform the JSON into native types.

Next, you need to make sure that models that you wish to decode from JSON conform to the JSONDecodable protocol:

public protocol JSONDecodable {
  typealias DecodedType = Self
  class func decode(JSONValue) -> DecodedType?
}

You will need to implement the decode function to perform any kinds of transformations you need to transform your model from a JSON value. A simple implementation for an enum value might look like:

enum RoleType: String {
  case Admin = "Admin"
  case User = "User"
}

extension RoleType: JSONDecodable {
  static func decode(j: JSONValue) -> RoleType? {
    switch j {
    case let .JSONString(s): return RoleType(rawValue: s)
    default: return .None
    }
  }
}

The real power of Argo can be seen when decoding actual model objects. To illustrate this, we will decode the simple User object that we used earlier.

Create your model normally:

struct User {
  let id: Int
  let name: String
}

You will also want to create a curried creation function. This is needed because of a deficiency in Swift that doesn't allow us to pass init functions around like other functions.

extension User {
  static func create(id: Int)(name: String) -> User {
    return User(id: id, name: name)
  }
}

Using this curried syntax will allow us to partially apply the function over the course of the decoding process. If you'd like to learn more about currying, we recommend the following articles:

The last thing to do will be to conform to JSONDecodable and implement the required decode function. We will implement this function by using map (<^>) and apply (<*>) to conditionally pass the required parameters to the creation function. The common pattern will look like:

return Model.create <^> paramOne <*> paramTwo <*> paramThree

and so on. If any of those parameters are .None, the entire creation process will fail, and the function will return .None. If all of the parameters are .Some(value), the value will be unwrapped and passed to the creation function.

In order to help with the decoding process, Argo introduces two new operators for parsing a value out of the JSON:

  • <| will attempt to parse a single value from the JSON
  • <|| will attempt to parse an array of values from the JSON

The usage of these operators is the same regardless:

  • json <| "key" is analogous to json["key"]
  • json <| ["key", "nested"] is analogous to json["key"]["nested"]

Both operators will attempt to parse the value from the JSON and will also attempt to cast the value to the expected type. If it can't find a value, or if that value is of the wrong type, the function will return .None.

There are also Optional versions of these operators:

  • <|? will attempt to parse an optional value from the JSON
  • <||? will attempt to parse an optional array of values from the JSON

Usage is the same as the non-optionals. The difference is that if these fail parsing, the parsing continues. This is useful for including parameters that truly are optional values. For example, if your system doesn't require someone to supply an email address, you could have an optional property: let email: String? and parse it with json <|? "email".

So to implement our decode function, we can use the JSON parsing operator in conjunction with map and apply:

extension User: JSONDecodable {
  static func decode(j: JSONValue) -> User? {
    return User.create
      <^> j <| "id"
      <*> j <| "name"
  }
}

For comparison, this same function without Argo would look like so:

extension User {
  static func decode(j: NSDictionary) -> User? {
    if let id = j["id"] as Int {
      if let name = j["name"] as String {
        return User(id: id, name: name)
      }
    }
  }

  return .None
}

You could see how this would get much worse with a more complex model.

You can decode custom types the same way, as long as the type also conforms to JSONDecodable.

For more examples on how to use Argo, please check out the tests.

About

Functional JSON parsing library for Swift

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 94.5%
  • Ruby 3.7%
  • C++ 1.8%