Alamofire response object mapping

11,788

Solution 1

You can use AlamofireMapper:

With json:

{
   "page":1,
   "per_page":3,
   "total":12,
   "total_pages":4,
   "data":[
      {
         "id":1,
         "first_name":"George",
         "last_name":"Bluth",
         "avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg"
      },
      {
         "id":2,
         "first_name":"Janet",
         "last_name":"Weaver",
         "avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"
      },
      {
         "id":3,
         "first_name":"Emma",
         "last_name":"Wong",
         "avatar":"https://s3.amazonaws.com/uifaces/faces/twitter/olegpogodaev/128.jpg"
      }
   ]
}

Swift class:

class UserResponse: Decodable {
    var page: Int!
    var per_page: Int!
    var total: Int!
    var total_pages: Int!

    var data: [User]?
}

class User: Decodable {
    var id: Double!
    var first_name: String!
    var last_name: String!
    var avatar: String!
}

Request with alamofire

let url1 = "https://raw.githubusercontent.com/sua8051/AlamofireMapper/master/user1.json"
        Alamofire.request(url1, method: .get
            , parameters: nil, encoding: URLEncoding.default, headers: nil).responseObject { (response: DataResponse<UserResponse>) in
                switch response.result {
                case let .success(data):
                    dump(data)
                case let .failure(error):
                    dump(error)
                }
        }

Link: https://github.com/sua8051/AlamofireMapper

Solution 2

Generic Response Object Serialization using Swift 4 Codable

If you don't want to use another dependency like ObjectMapper you can do the following way but you may have to make some chagnes.


Following is a typical model which we use to deserialize JSON data with generics using Alamofire. There is plenty of examples and excellent documentation on Alamofire.

struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
    let username: String
    let name: String

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }

    init?(response: HTTPURLResponse, representation: Any) {
        guard
            let username = response.url?.lastPathComponent,
            let representation = representation as? [String: Any],
            let name = representation["name"] as? String
        else { return nil }

        self.username = username
        self.name = name
    }
}

Using Codable protocol introduced in Swift 4

typealias Codable = Decodable & Encodable

The first step in this direction is to add helper functions that will do half of the work in deserialization JSON data and handle errors. Using Swift extensions we add functions to decode incoming JSON into our model struct/class that we will write afterward.

let decoder = JSONDecoder()
let responseObject = try? decoder.decode(T.self, from: jsonData)
The decoder (1) is an object that decodes instances of a data type from JSON objects.

Helper functions

extension DataRequest{
    /// @Returns - DataRequest
    /// completionHandler handles JSON Object T
    @discardableResult func responseObject<T: Decodable> (
        queue: DispatchQueue? = nil ,
        completionHandler: @escaping (DataResponse<T>) -> Void ) -> Self{

        let responseSerializer = DataResponseSerializer<T> { request, response, data, error in
            guard error == nil else {return .failure(BackendError.network(error: error!))}

            let result = DataRequest.serializeResponseData(response: response, data: data, error: error)
            guard case let .success(jsonData) = result else{
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            // (1)- Json Decoder. Decodes the data object into expected type T
            // throws error when failes
            let decoder = JSONDecoder()
            guard let responseObject = try? decoder.decode(T.self, from: jsonData)else{
                return .failure(BackendError.objectSerialization(reason: "JSON object could not be serialized \(String(data: jsonData, encoding: .utf8)!)"))
            }
            return .success(responseObject)
        }
        return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)
    }

     /// @Returns - DataRequest
    /// completionHandler handles JSON Array [T]
    @discardableResult func responseCollection<T: Decodable>(
        queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<[T]>) -> Void
        ) -> Self{

        let responseSerializer = DataResponseSerializer<[T]>{ request, response, data, error in
            guard error == nil else {return .failure(BackendError.network(error: error!))}

            let result = DataRequest.serializeResponseData(response: response, data: data, error: error)
            guard case let .success(jsonData) = result else{
                return .failure(BackendError.jsonSerialization(error: result.error!))
            }

            let decoder = JSONDecoder()
            guard let responseArray = try? decoder.decode([T].self, from: jsonData)else{
                return .failure(BackendError.objectSerialization(reason: "JSON array could not be serialized \(String(data: jsonData, encoding: .utf8)!)"))
            }

            return .success(responseArray)
        }
        return response(responseSerializer: responseSerializer, completionHandler: completionHandler)
    }
 }

Second, I earlier mentioned “using Swift 4 Codable” but if all we want is to decode JSON from the server, we only need is a model struct/class that conforms to protocol Decodable. (If you have the same structure you want to upload you can use Codable to handle both decoding and encoding) So, now our User model struct now looks like this.

struct User: Decodable, CustomStringConvertible {
    let username: String
    let name: String

    /// This is the key part
    /// If parameters and variable name differ
    /// you can specify custom key for mapping "eg. 'user_name'"

    enum CodingKeys: String, CodingKey {
        case username = "user_name"
        case name
    }

    var description: String {
        return "User: { username: \(username), name: \(name) }"
    }
}

Lastly, our function call to API looks like.

Alamofire.request(Router.readUser("mattt"))).responseObject{ (response: DataResponse<User>) in

            // Process userResponse, of type DataResponse<User>:
            if let user = response.value {
                print("User: { username: \(user.username), name: \(user.name) }")
            }
}

For more complex (nested) JSON, the logic remains the same and only modifications you need in model struct/class is that all structs/classes must conform to Decodable protocol and Swift takes care of everything else.

Share:
11,788
Syn3sthete
Author by

Syn3sthete

I am a Fullstack developer working for a Fintech in Mumbai. SOreadytohelp

Updated on June 08, 2022

Comments

  • Syn3sthete
    Syn3sthete almost 2 years

    I am an android developer new to swift 3 programming, I am using Alamofire for making api calls and to avoid tedious json paring I am using AlamofireObjectMapper library. I have a ApiController which has a function to make api calls below is the code for that:

    public static func makePostRequest<T: Mappable>(url: String, params: Parameters, networkProtocol: NetworkProtocol, responseClass: T){
    
        let headers = getHeaders()
    
        networkProtocol.showProgress()
    
        Alamofire.request(url, method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers)
            .validate()
            .responseData{ response in
                let json = response.result.value
                var jsonString = String(data: json!, encoding: String.Encoding.utf8)
                let responseObject = responseClass(JSONString: jsonString!)
                switch(response.result){
                case .success(_):
                    networkProtocol.hideProgress()
                    networkProtocol.onResponse(response: response)
                    break
                case .failure(_):
                    networkProtocol.hideProgress()
                    networkProtocol.onErrorResponse(response: response)
                    break
                }
    
        }
    

    The Json response template I am getting from server is:

    {
     "some_int": 10, 
     "some_array":[...]
    }
    

    Below is my model class:

    import ObjectMapper
    
        class BaseResponse: Mappable {
        var some_int: Int?
        var some_array: [Array_of_objects]?
    
        required init?(map: Map) {
            some_int <- map["some_int"]
            some_array <- map["some_array"]
        }
    
        func mapping(map: Map) {
    
        }
    }
    

    And below is the function to class make the api call:

    public static func callSomeApi(params: Parameters, networkProtocol: NetworkProtocol){
        ApiHelper.makePostRequest(url: AppConstants.URLs.API_NAME, params: params, networkProtocol: networkProtocol, responseClass: BaseResponse)
    }
    

    Now the error is in the below line

    let responseObject = responseClass(JSONString: jsonString!)
    

    I am not able to understand how to convert jsonString into the responseClass generic object which I am accepting from View controller

    Someone please help me resolve this, stuck on this issue for quite a while now.

  • Syn3sthete
    Syn3sthete almost 7 years
    The response is not an array it's just a json object. { "some_int": 10 "some_array":[...] } This is the template of Json response I am getting from server