Argo uses Swift's type system along with concepts from functional programming to let you smoothly transform JSON data into Swift model types. Argo 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!
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
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 Any
using Foundation
's
NSJSONSerialization
API. Once you have the Any
, you can call Argo's
global decode
function to get back the decoded model.
let json: Any? = try? NSJSONSerialization.JSONObjectWithData(responseData, options: [])
if let j: Any = 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 Any
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.
In order for this to work with your own model types, 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 type called
User
. Start by creating this User
model:
struct User {
let id: Int
let name: String
}
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.
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 heavy
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
These are infix operators that correspond to familiar operations:
json <| "name"
is analogous tojson["name"]
, in cases where a single item is associated with the"name"
keyjson <|| "posts"
is analogous tojson["posts"]
, in cases where an array of items is associated with the"posts"
key
As a bonus, if your JSON contains nested data whose 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. If a value is found, the operator will then attempt to cast the
value to the expected type. If that all works out, the operator will return the
decoded object wrapped inside a Decoded<T>.Success(.Some(value))
. If the value
it finds is of the wrong type, the function will return a
Decoded<T>.Failure(.TypeMismatch(expected: String, actual: String))
failure state. If
it can't find any value at all for the specified key, the function will return a
Decoded<T>.Failure(.MissingKey(name: 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.
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
also doesn't return to the caller any indication of where or why any failures
occur. This technique also requires you to specify the type of
each value in multiple places, which means that if the type of one of your
values changes, you'll have to change it at multiple places in your code. If
you're using Argo, however, you just need to declare the types of your
properties in your model, and then the Swift compiler will infer the types that
need to be sent to the curried decode
function and therefore the types that
need to be found in the JSON structure.
You can decode custom types the same way, as long as the type also conforms to
Decodable
. This is how we implement relationships.
For more Argo usage examples, see our test suite.