Strongly typed API objects using Argo

Nicky Gerritsen

April 27th, 2016

When I wanted to create a Swift SDK for our video streaming service, I knew I had to deal with the fact that JSON is completely loosely typed and does not work so nicely with Swift out of the box.

After some research, I came across Argo and decided to use it in our SDK. This blog post will explain what Argo is and how you can use it in your own project.

Introduction

The Argo Github repository describes Argo as follows:

[...] a library that lets you extract models from JSON or similar structures in a way that's concise, type-safe, and easy to extend.

Argo uses a lot of functional programming paradigms. This results in a library with a very lean and short syntax but can also mean that it is not completely clear what the library does and how to use it.

The map and apply functions

Argo makes heavy use of map from the Swift library, and it is important to know what it does. They also define their own apply function.

The map function, as the name suggests, maps a function over some data. For example, map on CollectionType is defined as[1]:

extension CollectionType {
    func map<T>(transform: (Self.Generator.Element) -> T) -> [T]
}

On Optional, it is defined as[2]:

extension Optional {
    func map<U>(transform: Wrapped -> U) -> U?
}

Using it is easy, as in the following code:

let data = [1, 2, 3, 4]
let newData = data.map { $0 * 2 }
// newData is now [2, 4, 6, 8]

[1] I have simplified the declaration a bit here.

[2] Wrapped is the type of element wrapped in the optional. Take a look at the following:

let optData: Int? = 3
let newOptData = optData.map { $0 + 5 }
/// newOptData is now an optional Int with value 8

The apply function is similar to map, but the transform function is different. Let's look at how apply is defined in Optional:

extension Optional {
    func apply<U>(transform: (Wrapped -> U)?) -> U? {
        switch transform {
        case .Some(let fx): return self.map(fx)
        case .None: return .None
        }
    }
}

As can be seen, apply just checks whether the function is nonnil, and if so, it maps it. Otherwise, it returns nil.

Argo defines the <^> operator for map and <*> for apply. This means that the following two statements are the same (using the same data as before)[3]:

let x = data.map { $0 + 2 }
let y = { $0 + 2 } <^> data
// x == y

Function currying

Another functional programming paradigm that Argo uses is function currying. This seems hard, but it can be explained with a simple example. Imagine we have the following function:

func add(a: Int, b: Int) -> Int {
    return a + b
}

However, what if we want a function that always adds four using the previous function? Normally, we would do the following:

func addFour(a: Int) -> Int {
    return add(a, 4)
}

However, we now also want such a function for 10, 11, 1337, and 245234. It does not make sense to define a new function for every value. In comes function currying! Let's modify the add function a bit, as follows:

[3]the operator versions take the function as first argument

func add(a: Int) -> (Int -> Int) {
    return { b in a + b }
}

The function now returns a function that we can use, as follows:

let addFour = add(4)
let val = addFour(5)
// or:
let val = add(4)(5)

The Curry github repository actually defines a really nice curry function that can be used to transform a function with arguments into a function with one less argument but returning a function instead of a value.

For more information about function currying in Swift, refer to this blog post by Thoughtbot.

Using Argo

OK, now that we are finally done with the very big introduction, we can start using Argo. Imagine we have the following JSON string, which we will parse into AnyObject using NSJSONSerialization[4]:

let json = "{\"status\": 0, \"message\": \"All OK\", \"data\": {\"values\": [1, 3, 4]}}"
let obj = try! NSJSONSerialization.JSONObjectWithData(json.dataUsingEncoding(NSUTF8StringEncoding)!, options: [])

Let's define struct[5] , which should be able to hold the data for this JSON object:

struct ExampleResult {
    let status: Int
    let message: String
    let values: [Int]?
}

Note that values is optional as it might be possible that there are no values in the input[6].

Now, to be able to parse the JSON object into a Result struct, the only thing we need to do is make ExampleResult conform to the Decodable protocol, as follows:

extension DemoResult : Decodable {
    static func decode(json: JSON) -> Decoded<DemoResult> {

[4]Error handling is ignored here; never use this or try it unless you have a good reason!

[5]A class is also possible, of course.

[6]This is just for illustrative purposes. Take a look at the following code:

        return curry(self.init)
<^> json <| "status"
<*> json <| "message"
<*> json <||? ["data", "values"]
    }
}

As can be seen, you only need to implement a decode function that takes a value of the JSON type (an enum that Argo defines that any JSON object will be transformed into to make it semi-typed) and produces a value of the Decoded enum type. Decoded is an enum that holds either the desired value or some failure.

As can also be seen, it uses the curry function to transform the initializer[7] into a fully curried version. After this, it uses two more custom operators to extract values from the JSON object and pass them as arguments to the curried init function using the map and apply operators from before.

In the end, you can use the following two ways to actually extract the JSON data:

let demoResult: DemoResult? = decode(obj)
// demoResult is now an optional DemoResult, in this case it contains the parsed data as decoding was successfull

let demoResultDecoded: Decoded<DemoResult> = decode(obj)
// demoResultDecoded is an enum on which you can call `error` and `value` to extract either the error or the parsed value.
// You can also switch on the enum cases

Conclusion

Although the technical ideas behind Argo are nontrivial to understand, using it is quite easy. The guys at Thoughtbot have done a tremendous job creating this super nice library to parse JSON into typed structures and classes. To find out more about how I used it in our SDK, check the readme and source code on Github.

About the author

Nicky Gerritsen is currently a software architect at StreamOne, a small Dutch company specializing in video streaming and storage. In his spare time, he loves to write code on Swift projects and learn about new things in Swift. He can be found on Twitter at @nickygerritsen and on Github here.

comments powered by Disqus