Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending array of dictionaries with alamofire

I have to send array of dictionaries via POST request. For example:

materials: [[String, String]] = [
  [
    "material_id": 1,
    "qty": 10
  ],
  [
    "material_id": 2,
    "qty": 5
  ]
]

Alamofire.request sends the next post data:

materials => array(
  [0] => array("material_id" => 1),
  [1] => array("qty" => 10),
  [2] => array("material_id" => 2),
  [3] => array("qty" => 5),
)

I want receive that representation:

materials => array(
  [0] => array(
    "material_id" => 1,
    "qty" => 10
  ),
  [1] => array(
    "material_id" => 2,
    "qty" => 5
  ),
)
like image 355
Nemmo Avatar asked Jan 06 '15 08:01

Nemmo


2 Answers

The problem was in append method. I have coded on PHP 5 years and forgoted that in Swift the indexes not automatically assigned like in PHP. So, my first bugged code was:

func getParameters() -> [[String: AnyObject]] {
    var result = [[String: AnyObject]]()

    for mmap in mmaps {
        let material: [String: AnyObject] = [
            "material_id": mmap.material.id,
            "quantity": mmap.qty
        ]
        result.append(material)
    }

    return result
}

The answer is hard assign the keys as you need:

func getParameters() -> [String: [String: AnyObject]] {
    var result = [String: [String: AnyObject]]()

    let mmaps = self.mmaps.allObjects as [Mmap]
    for i in 0..<mmaps.count {
        let mmap = mmaps[i]
        let material: [String: AnyObject] = [
            "material_id": mmap.material.id,
            "quantity": mmap.qty
        ]
        result["\(i)"] = material
    }

    return result
}
like image 196
Nemmo Avatar answered Nov 07 '22 15:11

Nemmo


A couple of thoughts:

  1. It would be easiest if you sent the response as a dictionary with one key, and it will correctly encode the array within the dictionary:

    let materials = [ "materials":
        [
            [
                "material_id": 1,
                "qty": 10
            ],
            [
                "material_id": 2,
                "qty": 5
            ]
        ]
    ]
    

    You could then just supply that as the parameters of request(), and Alamofire will properly encode that for you.

  2. If you wanted to send an array of dictionaries, an alternative would be to change the web service to accept JSON. You could then encode the JSON yourself (using JSONSerialization or JSONEncoder), set the body of the request, and then send that request.

  3. If you want to send application/x-www-form-urlencoded request with the array of dictionaries, you'd have to encode that yourself. In Swift 3 and later, that might look like:

    func encodeParameters(_ object: Any, prefix: String? = nil) -> String {
        if let dictionary = object as? [String: Any] {
            return dictionary.map { key, value -> String in
                self.encodeParameters(value, prefix: prefix != nil ? "\(prefix!)[\(key)]" : key)
                }.joined(separator: "&")
        } else if let array = object as? [Any] {
            return array.enumerated().map { (index, value) -> String in
                return self.encodeParameters(value, prefix: prefix != nil ? "\(prefix!)[\(index)]" : "\(index)")
            }.joined(separator: "&")
        } else {
            let escapedValue = "\(object)".addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)!
            return prefix != nil ? "\(prefix!)=\(escapedValue)" : "\(escapedValue)"
        }
    }
    

    Where

    extension CharacterSet {
    
        /// Returns the character set for characters allowed in the individual parameters within a query URL component.
        ///
        /// The query component of a URL is the component immediately following a question mark (?).
        /// For example, in the URL `http://www.example.com/index.php?key1=value1#jumpLink`, the query
        /// component is `key1=value1`. The individual parameters of that query would be the key `key1`
        /// and its associated value `value1`.
        ///
        /// According to RFC 3986, the set of unreserved characters includes
        ///
        /// `ALPHA / DIGIT / "-" / "." / "_" / "~"`
        ///
        /// In section 3.4 of the RFC, it further recommends adding `/` and `?` to the list of unescaped characters
        /// for the sake of compatibility with some erroneous implementations, so this routine also allows those
        /// to pass unescaped.
    
        static var urlQueryValueAllowed: CharacterSet = {
            let generalDelimitersToEncode = ":#[]@"    // does not include "?" or "/" due to RFC 3986 - Section 3.4
            let subDelimitersToEncode = "!$&'()*+,;="
    
            var allowed = CharacterSet.urlQueryAllowed
            allowed.remove(charactersIn: generalDelimitersToEncode + subDelimitersToEncode)
            return allowed
        }()
    }
    

    Obviously, use whatever response method is appropriate for the nature of your server's response (e.g. response vs. responseJSON vs. ...).

    Anyway, the above generates a request body that looks like:

    materials[0][material_id]=1&materials[0][qty]=10&materials[1][material_id]=2&materials[1][qty]=5
    

    And this appears to be parsed by servers as you requested in your question.

It's worth noting that this final point illustrates the preparation of an application/x-www-form-urlencoded request with nested dictionary/array structure, as contemplated here. This works on my server run by a major ISP, but I must confess that I haven't seen this convention documented in formal RFCs, so I'd be wary of doing it. I'd personally be inclined to implement this as JSON interface.

For prior versions of Swift, see previous revision of this answer.

like image 35
Rob Avatar answered Nov 07 '22 16:11

Rob