Skip to content

Instantly share code, notes, and snippets.

@jpotts18
Last active August 17, 2020 15:44
Show Gist options
  • Star 70 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save jpotts18/e39ee74de84ae094b270 to your computer and use it in GitHub Desktop.
Save jpotts18/e39ee74de84ae094b270 to your computer and use it in GitHub Desktop.
Alamofire JSON Serialization of Objects, Collections, Nesting

Alamofire JSON Serialization of Objects and Collections

Alamofire is a great Swift library developed by the creator of AFNetworking @mattt. The purpose of this gist is to explain how to use the built-in power of Alamofire to serialize your JSON. In this example we will be serializing a simple blog API. First we will start with serializing a single JSON object and add complexity as we go along.

Warning: This was written before Swift 1.2

A Single JSON Serialization

This is the first JSON object that we will be serializing.

// GET posts/1.json
{
    "id": 1,
    "title": "Post #1",
    "body": "My first blog post"
}

In order to use the built-in JSON Serialization of Alamofire we need to extend the default functionality of the Alamofire.Request object with this snippet. This snippet is taken from the Alamofire documentation called Generic Response Object Serialization.

// ResponseObjectSerializable.swift

@objc public protocol ResponseObjectSerializable {
    init?(response: NSHTTPURLResponse, representation: AnyObject)
}

extension Alamofire.Request {
    public func responseObject<T: ResponseObjectSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, T?, NSError?) -> Void) -> Self {
        let serializer: Serializer = { (request, response, data) in
            let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
            let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
            if response != nil && JSON != nil {
                return (T(response: response!, representation: JSON!), nil)
            } else {
                return (nil, serializationError)
            }
        }

        return response(serializer: serializer, completionHandler: { (request, response, object, error) in
            completionHandler(request, response, object as? T, error)
        })
    }
}

Now that we can call the responseObject on Alamofire.Request object, we need to implement the ResponseObjectSerializable protocol. This protocol tells Alamofire how to map JSON values in the representation object to instance variables in the Post model. Make sure you use the keyword final and immutable variables with let.

// Post.swift

final class Post: ResponseObjectSerializable {
    let id: Int
    let title: String
    let body: String

    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.id = representation.valueForKeyPath("id") as Int
        self.title = representation.valueForKeyPath("title") as String
        self.body = representation.valueForKeyPath("body") as String
    }
}

You can now put all of the pieces together and make a request that implements the responseObject method and serializes your Post object.

Alamofire.request(.GET, "http://example.com/posts/1.json")
         .responseObject { (_, _, post: Post?, _) in
             println(post)
         }

Serializing JSON Collections

So that wasn’t so bad, but building a great app requires pulling in a lot more than just one object. Most of the time you will need to pull in a collection of objects. Here is an example JSON collection of post objects that we will now serialize.

// GET /posts.json
[
    {
        "id": 1,
        "title": "Post #1",
        "body": "My first blog post"
    },
    {
        "id": 2,
        "title": "Post #2",
        "body": "My second blog post"
    },
    {
        "id": 3,
        "title": "Post #3",
        "body": "My third blog post"
    }
]

To serialize a collection of objects we will need to extend the Alamofire.Request object again in order to add the responseCollection method to our network requests. Once again I will paste in the code from the documentation under Generic Response Object Serialization.

// ResponseCollectionSerializable.swift

@objc public protocol ResponseCollectionSerializable {
    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}

extension Alamofire.Request {
    public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
        let serializer: Serializer = { (request, response, data) in
            let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
            let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
            if response != nil && JSON != nil {
                return (T.collection(response: response!, representation: JSON!), nil)
            } else {
                return (nil, serializationError)
            }
        }

        return response(serializer: serializer, completionHandler: { (request, response, object, error) in
            completionHandler(request, response, object as? [T], error)
        })
    }
}

We have now added the responseCollection to the Alamofire.Request object. Next we need to go and implement the ResponseCollectionSerializable protocol. We will build from our existing Post.swift file.

// Post.swift

// Note: we are now implemented the ResponseCollectionSerialiable protocol
final class Post: ResponseObjectSerializable, ResponseCollectionSerializable {
    let id: Int
    let title: String
    let body: String

    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.id = representation.valueForKeyPath("id") as Int
        self.title = representation.valueForKeyPath("title") as String
        self.body = representation.valueForKeyPath("body") as String
    }

    // This is where the magic happens
    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Post] {
        var postArray = representation as [AnyObject]
        // using the map function we are able to instantiate Post while reusing our init? method above
        return postArray.map({ Post(reponse:response, representation: $0) })
    }
}

Now when we make the Alamofire request we can call the responseCollection method. Then Alamofire will run our collection method which loops through our posts array and calls the init? method on each. It will then return an array of Post objects.

Alamofire.request(.GET, "http://blog.com/posts.json")
         .responseCollection { (_, _, posts: [Post]?, error) in
             println(posts)
         }

Serializing a Nested JSON

Being able to serialize collections gives an app more power, but still a JSON collection will most likely have nested objects and collections inside of it. Alamofire supports this as well, but requires more work in our model layer. Here is the JSON that we want to serialize. Notice that it has an author object inside of it and an array of comments.

// GET /posts/1.json
{
    "id": 1,
    "title": "Alamofire JSON Serialization",
    "body": "All about serialization in Alamofire...",
    "author": {
        "id", 1,
        "full_name": "Jeff Potter",
        "user_name": "jpotts18"
    },
    "comments": [
        {
            "id": 1,
            "body": "Thanks for the help Jeff, this saved me hours"
        },
        {
            "id": 2,
            "body": "Your welcome. I am happy to help!"
        }
    ]
}

A Nested JSON Object

Our JSON now has nested objects inside of itself. We will now serialize the author JSON object. To start off we will add our Author model, then implement the ResponseObjectSerializable, and finally add our Author object into the Post object.

// Author.swift

// Implement the protocol
final class Author: ResponseObjectSerializable {
    
    let id: Int
    let fullName: String
    let userName: String
    
    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        // map the values to the instance
        self.id = representation.valueForKeyPath("id") as Int
        self.fullName = representation.valueForKeyPath("full_name") as String
        self.userName = representation.valueForKeyPath("user_name") as String
    }

}

Now that the Author object can be serialized we can add it to our Post.init? method.

// Post.swift

final class Post: ResponseObjectSerializable, ResponseCollectionSerializable {
    let id: Int
    let title: String
    let body: String
    let author: Author // Adding the author here

    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.id = representation.valueForKeyPath("id") as Int
        self.title = representation.valueForKeyPath("title") as String
        self.body = representation.valueForKeyPath("body") as String
        // We will instantiate the Author object using Author.init?
        self.author = Author(response:response, representation: representation.valueForKeyPath("author")!)!
    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Post] {
        var postArray = representation as [AnyObject]
        return postArray.map({ Post(reponse:response, representation: $0) })
    }
}

Nested JSON Collection

Now that we know how to do a nested Author object inside of the Post, we can move on to serializing the comments collection. First we will need to add the Comment model and implement the init? and collection methods.

// Comment.swift

final class Comment: ResponseObjectSerializable, ResponseCollectionSerializable {
    let id: Int
    let body: String

    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.id = representation.valueForKeyPath("id") as Int
        self.body = representation.valueForKeyPath("body") as String
    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Comment] {
        var commentArray = representation as [AnyObject]
        return commentArray.map({ Comment(reponse:response, representation: $0) })
    }
}

Now we need to go and add our Comments array into our Post model. In our init? method we will pluck the comments array from the representation and pass it to Comment.collection for serialization.

// Post.swift

final class Post: ResponseObjectSerializable, ResponseCollectionSerializable {
    let id: Int
    let title: String
    let body: String
    let author: Author // nested object
    let comments: [Comment] // nested array

    required init?(response: NSHTTPURLResponse, representation: AnyObject) {
        self.id = representation.valueForKeyPath("id") as Int
        self.title = representation.valueForKeyPath("title") as String
        self.body = representation.valueForKeyPath("body") as String
        self.author = Author(response:response, representation: representation.valueForKeyPath("author")!)!
        // Pluck the comments array from the representation and pass it to Comment.collection for serialization.  
        self.comments = Comment.collection(response:response, representation: representation.valueForKeyPath("comments")!)!
    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Post] {
        var postArray = representation as [AnyObject]
        return postArray.map({ Post(reponse:response, representation: $0) })
    }
}

We are now ready to make our network request using Alamofire and let it handle all of the nested serialization and instantiation behind the scenes. Here is what the request will look like.

Alamofire.request(.GET, "http://blog.com/posts/1.json")
         .responseCollection { (_, _, nestedPost: Post?, error) in
             println(nestedPost)
         }

Conclusion

Alamofire provides a simple way to map JSON representations to instance objects. By implementing the ResponseObjectSerializable and ResponseCollectionSerializable protocols you can encapsulate all of your object instantiation and collection logic in your data model layer.

@glagnar
Copy link

glagnar commented Apr 16, 2015

Thank you very much for this 'summation' and very useful guide. I have one critical comment, and that is that I had to make several changes for it to work in Swift 1.2 - or am I just 'doing it wrong' ?

@abunur
Copy link

abunur commented Jul 2, 2015

why not just make use of the built-in responseJSON() ?

@sacdallago
Copy link

@jpotts18 I think this guide is really good, but I am completely new to swift coding and I get into errors starting from the first snippet :(

I created ResponseObjectSerializable.swift and pasted the above code as-is adding imports for Foundation and Alamofire but I get:

  • Invalid redeclaration of 'ResponseObjectSerializable'
  • 'ResponseObjectSerializable' is ambiguous for type lookup in this context
  • Use of undeclared type 'Serializer'

maybe I need to import something else? :p

Thanks and keep the good work going :)

@alexandruzanfir
Copy link

Can you update this example for alamofire 4, please.

@steffen25
Copy link

steffen25 commented Oct 14, 2016

+1 for Alamofire 4 support

@robseward
Copy link

+1 for AF 4

@parleer
Copy link

parleer commented Oct 31, 2016

+1 for AF 4

@ishell
Copy link

ishell commented Nov 16, 2016

@ljubinkovicd
Copy link

ljubinkovicd commented Oct 10, 2017

This is awesome Jeff. Very clear explanation. I just have one question:
This is how my init?(...) looks:

init?(response: HTTPURLResponse, representation: Any) { guard let representation = representation as? [String: Any], let title = representation["label"] as? String, let mealImgUrl = representation["image"] as? String, let redirectUrl = representation["url"] as? String, let calories = representation["calories"] as? Float, let source = representation["source"] as? String, let ingredients = Ingredient.collection(from: response, withRepresentation: representation["ingredients"]!) else { return nil } self.title = title self.mealImgUrl = mealImgUrl self.redirectUrl = redirectUrl self.calories = calories self.source = source self.ingredients = ingredients }

and it is throwing this error: Initializer for conditional binding must have Optional type, not '[String : Any]'

It happened after I added:
let ingredients = Ingredient.collection(from: response, withRepresentation: representation["ingredients"]!)
to the guard statement.

Any ideas on how to fix this, but with the guard still there?

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment