The first application I developed for iOS had not much difficulty when it came to business logic: as an app that mainly consumed data from a Rest API, the main challenges lay in two areas: provide an attractive, and usable user interface, and obtaining the data from a remote server.

The development of the user interface included the development of custom components, and spending the required time playing with Auto Layout.

The development of the part of the application responsible for fetching data from the server was greatly simplified by using the Alamofire library.

But fetching the data was only the first step: convert the retrieved JSON to objects was the next challange: enter ObjectMapper .

ObjectMapper

ObjectMapper is a framework than can convert JSON to objects (and viceversa).

If we don’t use a mapper library we must parse the JSON manually. Although that can be done easily enough (using libraries for manipulating JSON data like SwiftyJSON ), it can be achieved even more easily, and quickly, using a library like ObjectMapper. Lets see how.

Lets start by retrieving some JSON data

I am going to download a list of users from JSONPlaceholder, a free online REST service that can be used to obtain some fake data for testing purposes, etc. One of the test urls that they provide serves a list of users that can be used as a fictional list of customers.

Here is the REST request using Alamofire:

class CustomerService {
    static let sharedInstance = CustomerService()
    private var manager: SessionManager

    private init() {
        self.manager = Alamofire.SessionManager.default
    }

    func getCostumers(completion:@escaping (Array<Customer>) -> Void, failure:@escaping (Int, String) -> Void) -> Void{

        let url: String = "https://jsonplaceholder.typicode.com/users"

        self.manager.request(url).validate(statusCode: 200..<300).responseJSON { response in
            switch response.result {
            case .success:
                //process the JSON data

            case .failure(let error):
                failure(0,"Error \(error)")
            }
        }
    }

The code above merely retrieves the data from the server; notice I am validating the output to make sure that I enter in the .success branch only if the request is successful.

Let’s have a look at the retrieved JSON

The JSON I did obtain looks like this:

[
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
}
...
]

In my application I would use a Costumer object, and Address object, a Company object, etc. In other words something like this:

class Company{
    var name:String?
    var catchPhrase:String?
    var bs:String?

    //initializers, etc
}

// Address class, Geo class

class Customer
{
    let id: Int
    var name: String?
    var username:String?
    var email:String?
    var phone:String?
    var website:String?

    var company: Company?

    // rest of properties, initializers, etc
}

So what I want is to take the JSON I retrieved, and convert it into objects like the ones described just above.

Installing ObjectMapper

Just add the following line to your pod file, and run pod install.

pod 'ObjectMapper', '~> 2.2'

Now we can import, and use the library in our project

import ObjectMapper

Using ObjectMapper

In order to use ObjectMapper we need to implement the Mappable protocol:

public protocol BaseMappable {
	mutating func mapping(map: Map)
}
public protocol Mappable: BaseMappable {
    init?(map: Map)
}

So we need to modify a bit the code we created to model our objects:
1. Our objects need to extend from Mappable
2. Our objects need to implement the mapping function in which we will specify which properties of the JSON are assigned to which properties of the object.
3. Properties must be declared as optional variables

Our Company class would now look like this:

class Company:Mappable{
    var name:String?
    var catchPhrase:String?
    var bs:String?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        name         <- map["name"]
        catchPhrase  <- map["catchPhrase"]
        bs  <- map["bs"]
    }
}

And here is the complete data model:

class Company:Mappable{
    var name:String?
    var catchPhrase:String?
    var bs:String?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        name         <- map["name"]
        catchPhrase  <- map["catchPhrase"]
        bs  <- map["bs"]
    }
}

class Geo:Mappable
{
    var lat: String?
    var lng:String?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        lat         <- map["lat"]
        lng          <- map["lng"]
    }
}

class Address:Mappable
{
    var street: String?
    var suite:String?
    var city:String?
    var zipcode:String?
    var geo:Geo?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        street         <- map["street"]
        suite          <- map["suite"]
        city           <- map["city"]
        zipcode        <- map["zipcode"]
        geo            <- map["geo"]

    }
}

class Customer:Mappable
{
    var id: Int?
    var name: String?
    var username:String?
    var email:String?
    var phone:String?
    var website:String?

    var address:Address?
    var company: Company?

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        id            <- map["id"]
        name          <- map["name"]
        username      <- map["username"]
        email         <- map["email"]
        phone         <- map["phone"]
        website       <- map["website"]
        company        <- map["company"]
        address        <- map["address"]
    }
}

Notice that in the above mapping I tried to represent a data model that matched closely the structure of the data returned in the JSON. In real life,that does not need to be the case. For example, the object could be defined as this (not Geo class):

class Address
{
    var street: String?
    var suite:String?
    var city:String?
    var zipcode:String?
    var lat: String?
    var lng:String?
}

And the mapping matching to the above definition would be this:

class Address:Mappable
{
    var street: String?
    var suite:String?
    var city:String?
    var zipcode:String?
    var lat: String?
    var lng:String?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        street         <- map["street"]
        suite          <- map["suite"]
        city           <- map["city"]
        zipcode        <- map["zipcode"]
        lat        <- map["geo.lat"]
        lng        <- map["geo.lng"]
    }
}

Trasforming JSON data into objects

Our JSON can be converted into an array of Customer objects using the following command:

Mapper<Customer>().mapArray(JSONArray: responseJSON)

At this point we can finally complete our initial REST request to include the mapping of the result into objects:

import Alamofire
import ObjectMapper

class CustomerService {
    static let sharedInstance = CustomerService()
    private var manager: SessionManager
    
    
    private init() {
        self.manager = Alamofire.SessionManager.default
    }
    
    func getCostumers(completion:@escaping (Array<Customer>) -> Void, failure:@escaping (Int, String) -> Void) -> Void{
        let url: String = "https://jsonplaceholder.typicode.com/users"
        
        self.manager.request(url).validate(statusCode: 200..<300).responseJSON { response in
            switch response.result {
            case .success:
                //to get JSON return value
                guard let responseJSON = response.result.value as? Array<[String: AnyObject]> else {
                    failure(0,"Error reading response")
                    return
                }
                guard let customers:[Customer] = Mapper<Customer>().mapArray(JSONArray: responseJSON) else {
                    failure(0,"Error mapping response")
                    return
                }
              
                completion(customers)
            case .failure(let error):
                failure(0,"Error \(error)")
            }
        }
    }
}

In this case I needed to map the result to an array of objects. Sometimes, though, the JSON returned by the request will not represent an array, but merely a single object.

For example, instead of a list of users, I can request an user by id:

https://jsonplaceholder.typicode.com/users/2

The server will not return an array this time:

{
  "id": 2,
  "name": "Ervin Howell",
  "username": "Antonette",
  "email": "Shanna@melissa.tv",
  "address": {
    "street": "Victor Plains",
    "suite": "Suite 879",
    "city": "Wisokyburgh",
    "zipcode": "90566-7771",
    "geo": {
      "lat": "-43.9509",
      "lng": "-34.4618"
    }
  },
  "phone": "010-692-6593 x09125",
  "website": "anastasia.net",
  "company": {
    "name": "Deckow-Crist",
    "catchPhrase": "Proactive didactic contingency",
    "bs": "synergize scalable supply-chains"
  }
}

I will not use Mapper().mapArray(JSONArray: responseJSON) to map a single object, I will modify my code like this:

guard let responseJSON = response.result.value as? [String: AnyObject] else {
    failure(0,"Error reading response")
    return
}
let customer = Mapper<Customer>().map(JSONObject: responseJSON)

Inheritance

Using inheritance with ObjectMapper poses not particular problem: back to the example we have been using in this post, we could have our Customer class inherit from a more generic Person class:

class Person: Mappable {
    var name: String?
    var phone:String?
    var address:Address?

    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        name          <- map["name"]
        phone         <- map["phone"]
        address        <- map["address"]
    }
}


class Customer:Person
{
    var id: Int?
    var username:String?
    var email:String?
    var website:String?
    var company: Company?
    
    required convenience init?(map: Map) {
        self.init()
    }

    func mapping(map: Map) {
      super.mapping(map:map)
        id            <- map["id"]
        username      <- map["username"]
        email         <- map["email"]
        website       <- map["website"]
        company        <- map["company"]
    }
}

Hope it was of help :-). Which mapper library do you use on your daily swift projects? or do you prefer to do the mapping manually?

If you liked this post, you can subscribe to my blog (click at the follow button at the bottom ) so you can be notified of new posts.

Advertisements