Added some explanatory text here and there. Added subsection headers to partition things a bit. Updated code snippets to Swift 2 and to latest Argo usage.
7.6 KiB
Usage
Argo uses Swift's type system along with concepts from functional programming to let you smoothly transform JSON data into Swift model objects or structs. Argo's approach does this with a minimum of syntax, while at the same time improving type safety and data integrity compared to other approaches.
You may need to learn a few things in order to learn Argo effectively, but once you do so, you'll have a powerful new tool to hang on your belt!
Decoding basics
Argo's whole purpose is to let you easily pick apart structured data (normally
in the form of a dictionary created from JSON data) and create Swift objects or
structs based on the decoded content. Typically, you'll want to do this with
JSON data received from a server or elsewhere. The first thing you need to do is
convert the JSON data from NSData to an AnyObject using Foundation's
NSJSONSerialization API. Once you have the AnyObject, you can call Argo's
global decode function to get back the decoded model.
let json: AnyObject? = try? NSJSONSerialization.JSONObjectWithData(responseData, options: [])
if let j: AnyObject = json {
let user: User? = decode(j) // ignore failure info or
let decodedUser: Decoded<User> = decode(j) // preserve failure info
}
As you see in this example, Argo introduces a new type: Decoded<T>. This new
type contains either a successfully decoded object or a failure state that
preserves information about why a decoding failed. You can choose to either
ignore the Decoded type and just get back the optional value or keep the
Decoded type and use it to debug or report decoding failures. When you decode
an AnyObject into a model using the global decode function, you can specify
whether you want an Optional model or a Decoded model by specifying the
return type as seen in the code block above.
Implementing Decodable
In order for this to work with your own model classes or structs, you need to make
sure that models that you wish to decode from JSON conform to the Decodable
protocol:
public protocol Decodable {
typealias DecodedType = Self
static func decode(json: JSON) -> Decoded<DecodedType>
}
In your model, you need to implement the decode function to perform whatever
transformations are needed in order to create your model from the given JSON
structure. To illustrate this, we will decode a simple model object called
User. Start by creating this User model:
struct User {
let id: Int
let name: String
}
Currying User.init
We will be using another small library called Curry to help with decoding
our User model. Currying allows us to partially apply the init function over
the course of the decoding process. This basically means that we can build up
the init function call bit by bit, adding one parameter at a time, if and only
if Argo can successfully decode them. If any of the parameters don't meet our
expectations, Argo will skip the init call and return a special failure state.
If you'd like to learn more about currying, we recommend the following articles:
Now, we make User conform to Decodable and implement the required decode
function. We will implement this function by using some functional
concepts,
specifically the map (<^>) and apply (<*>) operators, to conditionally
pass the required parameters to the curried init function. The common pattern
will look like this:
static func decode(json: JSON) -> Decoded<DecodedType> {
return curry(Model.init) <^> paramOne <*> paramTwo <*> paramThree
}
and so on. If any of those parameters are a failure state, then the entire
creation process will fail, and the function will return the first failure
state. If all of the parameters are successful, the value will be unwrapped and
passed to the init function.
Safely pulling values from JSON
In the example above, we showed some non-existent parameters (paramOne, etc), but
one of Argo's main features is the ability to help you grab the real parameters
from the JSON structure in a way that is type-safe and concise. You don't need
to manually check to make sure that a value is non-nil, or that it's of the
right type. Argo leverages Swift's expressive type system to do that leaving
lifting for you. 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 simple:
json <| "name"is analogous tojson["name"]json <|| "posts"is analogous tojson["posts"]
As a bonus, if your JSON contains nested data whose intermediate elements are
also Decodable, then you can retrieve a nested value by using an array of
strings:
json <| ["location", "city"]is analogous tojson["location"]["city"]
Each of these operators will attempt to extract the specified value from the
JSON structure and if a value is found, the operator will also attempt to cast
the value to the expected type. If it can't find a value, the function will
return a Decoded.MissingKey(message: String) failure state. If the value it
finds is of the wrong type, the function will return a
Decoded.TypeMismatch(message: String) failure state.
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 for the non-optional variants. The difference is that if
these operators happen to pull a nil value from the JSON, they will consider
this a success and continue on, rather than returning a failure state. 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 on User such as let email: String? and use json <|? "email" to decode either an email string or a nil value.
Finally implementing your decode function
So to implement our decode function, we can use the JSON parsing operator in
conjunction with map and apply:
extension User: Decodable {
static func decode(j: JSON) -> Decoded<User> {
return curry(User.init)
<^> j <| "id"
<*> j <| "name"
}
}
For comparison, an implementation of a similar function without Argo could look like this:
extension User {
static func decode(j: NSDictionary) -> User? {
if let id = j["id"] as! Int?,
let name = j["name"] as! String?
{
return User(id: id, name: name)
}
return .None
}
}
Not only is that code much more verbose than the equivalent code using Argo,
it's also not as safe. It does check to make sure that id and name are
non-nil, but it's not checking to see that their types are correct. If that code
encountered JSON where the "id" key returned, say a String instead of an
Int, this would lead to a crash.
With a more complex model, this would just get worse.
You can decode custom types the same way, as long as the type also conforms to
Decodable.
For more examples on how to use Argo, please check out the tests.