jpotts18
jpotts18

Reputation: 5111

Alamofire Nested JSON Serializer

I am using Alamofire to make requests to a JSON API. I am trying to serialize a collection of Post objects that have an author object and a comments array inside of it.

Click to see the hosted JSON

I have done the following:

Step 1: Follow steps

Extended the Alamofire.Request object and added the ResponseObjectSerializer and ResponseCollectionSerializeras explained in the documentation under Generic Response Object Serialization

Step 2: Add the following models

Post.swift

final class Post : ResponseObjectSerializable, ResponseCollectionSerializable {

    let id: Int
    let title: String
    let body: String
    let author: Author
    let comments: [Comment]

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

        // What do I do with the author object

        var authorObj: AnyObject? = representation.valueForKeyPath("author")

        if (authorObj != nil) {
            self.author = Author(response: response, representation: authorObj!)!
        }

        // What do I do with the comments Array?
        self.comments = Comment.collection(response: response, representation: representation.valueForKeyPath("comments")!)

    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Post] {
        var postList:[Post] = []
        for p in representation as [AnyObject] {
            postList.append(Post(response: response, representation: p)!)
        }
        return postList
    }

}

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 commentList:[Comment] = []
        var commentArray = representation as [AnyObject]
        for c in commentArray {
            commentList.append(Comment(response: response, representation: c)!)
        }
        return commentList
    }

}

Author.swift

final class Author : ResponseObjectSerializable {

    let id: Int
    let name: String

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

Step 3: The representation is a Builtin.RawPointer

(lldb) po representation (instance_type = Builtin.RawPointer = 0x00007f8b9ae1d290 -> 0x000000010c7f4c88 (void *)0x000000010c7f4dc8: __NSArrayI)

Any suggestions?

Step 4: Here is how I am calling the code

class NetworkPostProvider {

    typealias RequestsCollectionResponse = (NSError?, [Post]?) -> Void

    class func all(onCompletion: RequestsCollectionResponse) {

        var manager = Alamofire.Manager.sharedInstance

        manager.session.configuration.HTTPAdditionalHeaders = [
            "Authorization": NSUserDefaults.standardUserDefaults().valueForKey(DEFAULTS_TOKEN) as String
        ]

        manager.request(.GET, BASE_URL + "/alamofire/nested")
            .validate()
            .responseCollection({ (req, res, requests:[Post]?, error) -> Void in
                println("Request:")
                println(req)
                println("Response:")
                println(res)

                println(requests)

            })
    }

}

Upvotes: 1

Views: 4167

Answers (1)

cnoon
cnoon

Reputation: 16643

There are definitely some issues with your parsing logic (mainly safety). I went through and re-created your scenario the best I could and reworked the portions that seemed problematic. I have managed to fix your parsing which I'm pretty sure is the issue. It looks like you're calling everything properly from the Alamofire perspective.

import Foundation
import Alamofire

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

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

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)
        })
    }

    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)
        })
    }
}

final class Post : ResponseObjectSerializable, ResponseCollectionSerializable, Printable {

    let id: Int
    let title: String
    let body: String
    let author: Author
    let comments: [Comment]

    var description: String {
        return "Post {id = \(self.id), title = \(self.title), body = \(self.body), author = \(self.author), comments = \(self.comments)}"
    }

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

        var author: Author?

        if let authorObject: AnyObject = representation.valueForKeyPath("author") {
            author = Author(response: response, representation: authorObject)
        }

        var comments: [Comment]?

        if let commentsObject: AnyObject = representation.valueForKeyPath("comments") {
            comments = Comment.collection(response: response, representation: commentsObject)
        }

        if id != nil && body != nil && title != nil && author != nil && comments != nil {
            self.id = id!
            self.title = title!
            self.body = body!
            self.author = author!
            self.comments = comments!
        } else {
            self.id = 0
            self.title = ""
            self.body = ""
            self.author = Author(id: 0, name: "")
            self.comments = [Comment]()
            return nil
        }
    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Post] {
        var postList = [Post]()

        for p in representation as [AnyObject] {
            if let post = Post(response: response, representation: p) {
                postList.append(post)
            }
        }

        return postList
    }
}

final class Comment : ResponseObjectSerializable, ResponseCollectionSerializable, Printable {

    let id: Int
    let body: String

    var description: String {
        return "Comment {id = \(self.id), name = \(self.body)"
    }

    init(id: Int, body: String) {
        self.id = id
        self.body = body
    }

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

        if id != nil && body != nil {
            self.id = id!
            self.body = body!
        } else {
            self.id = 0
            self.body = ""
            return nil
        }
    }

    class func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Comment] {
        var commentList = [Comment]()
        var commentArray = representation as [AnyObject]

        for c in commentArray {
            if let comment = Comment(response: response, representation: c) {
                commentList.append(comment)
            }
        }

        return commentList
    }

}

final class Author : ResponseObjectSerializable, Printable {

    let id: Int
    let name: String

    var description: String {
        return "Author {id = \(self.id), name = \(self.name)}"
    }

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

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

        if id != nil && name != nil {
            self.id = id!
            self.name = name!
        } else {
            self.id = 0
            self.name = ""
            return nil
        }
    }
}

class NetworkPostProvider {

    typealias RequestsCollectionResponse = (NSError?, [Post]?) -> Void

    class func all(onCompletion: RequestsCollectionResponse) {

        var manager = Alamofire.Manager.sharedInstance

        manager.session.configuration.HTTPAdditionalHeaders = [
            "Authorization": NSUserDefaults.standardUserDefaults().valueForKey("12345678") as String
        ]

        let request = manager.request(.GET, "Some base url" + "/alamofire/nested")
        request.validate()
        request.responseCollection { (req, res, requests: [Post]?, error) in
            println("Request:")
            println(req)
            println("Response:")
            println(res)

            println(requests)
        }
    }

    class func forceLoadJSON() {
        let path = NSBundle.mainBundle().pathForResource("stacked", ofType: "json")!
        let data = NSData(contentsOfFile: path)!
        if let json: AnyObject = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil) {
            println(json)
            let fakeResponse = NSHTTPURLResponse(URL: NSURL(string: "")!, statusCode: 200, HTTPVersion: nil, headerFields: nil)!
            let posts = Post.collection(response: fakeResponse, representation: json)
            println("Posts: \(posts)")
        } else {
            println("Failed to load the posts")
        }
    }
}

The forceloadJSON method recreates everything Alamofire will trigger once the request comes back from the server. The problem is that I can't actually call your server b/c you didn't provide the necessary credentials or URL to actually call it. If you want me to debug it further, I'll need that info. I'm almost positive though that your issue has already been resolved with all the changes I've made to the Post, Comment and Author classes.

Another change I would suggest moving forward would be to use structs instead of classes. There's currently a bug in the Swift 1.1 compiler that doesn't allow you to return nil in a failable initializer inside a class. This is most likely fixed in Swift 1.2 but I haven't checked yet.

The last thing I would change would be to modify the ResponseCollectionSerializable function collection to be able to return either an optional array or an array of optionals. This is because your collection method can fail to create instances since it is calling failable initializers. Currently it is burying those errors and you don't know if parsing ever failed. You previously would crash in that scenario (maybe that's what you want). I modified it so that it won't crash, but the error is then buried.

Upvotes: 4

Related Questions