Mapping JSON With Swift And Mapper

A few weeks ago, I wrote about Unbox, a lightweight library to convert JSON data to model objects. There is a lot to like about Unbox and it takes seconds to start using it in a project.

I recently came across Mapper, a similar library that is developed and maintained by the people at Lyft. The feature set of Mapper is very similar to that of Unbox. That is not surprising, though. Both libraries aim to solve the same problem, converting JSON data to strongly typed model objects.

In this tutorial, I refactor the weather application I created for Unbox, replacing Unbox with Mapper.

Project Setup

Clone or download the project from GitHub and open the project's Podfile in a text editor. Replace Unbox with ModelMapper.

platform :ios, '8.0'
use_frameworks!

target 'Forecast' do
  pod 'ModelMapper'
end

Run pod install from the command line to install the project's dependencies and open the workspace CocoaPods has created for you in Xcode. For this tutorial, I am working with Mapper 3.0.0.

Analyzing dependencies
Downloading dependencies
Installing ModelMapper (3.0.0)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total
pod installed.

Updating Model Objects

Similar to Unbox, model objects need to conform to a protocol, Mappable, to be compatible with Mapper. Open WeatherData.swift and update the import statement at the top.

import Mapper

The WeatherData structure needs to conform to the Mappable protocol.

import Mapper

struct WeatherData: Mappable {
    ...
}

The most important change we need to make is replacing the initializer we implemented to conform to the Unboxable protocol. Like Unbox, the initializer accepts an object that handles the unboxing or mapping of the JSON data.

import Mapper

struct WeatherData: Mappable {

    let lat: Double
    let long: Double
    let windSpeed: Double
    let fahrenheit: Double
    let hourlyDataPoints: [WeatherDataPoint]

    init(map: Mapper) throws {
        lat = try map.from("latitude")
        long = try map.from("longitude")

        windSpeed = try map.from("currently.windSpeed")
        fahrenheit = try map.from("currently.temperature")

        hourlyDataPoints = try map.from("hourly.data")
    }

}

Note that the initializer is throwing and so is the from(_:) method of the Mapper structure. The Mapper library supports key paths, but, unlike Unbox, we do not need to specify whether the string we pass to from(_:) is a key or a key path. As far as I can tell, Mapper does not support keys that include periods.

Like Unbox, Mapper supports nested models. Remember that hourlyDataPoints is an array of WeatherDataPoint objects. Open WeatherDataPoint.swift to conform the WeatherDataPoint structure to the Mappable protocol.

import Mapper

struct WeatherDataPoint: Mappable {

    let time: Int
    let windSpeed: Double
    let fahrenheit: Double

    init(map: Mapper) throws {
        time = try map.from("time")
        windSpeed = try map.from("windSpeed")
        fahrenheit = try map.from("temperature")
    }

}

Updating View Controller

Start by removing the import statement for the Unbox library at the top of ViewController.swift. The ViewController class does not need to know about Mapper for the library to do its work. That is a subtle yet important detail.

The changes we need to make are limited to the processWeatherData(_:) method in which we process the response we receive from the Forecast API. Unfortunately, Mapper does not know how to handle binary data. We need to hand it an array or dictionary of data. This is an important difference and for Cocoa project I prefer the approach Unbox takes. Remember that Unbox can handle structured data (an array or dictionary) as well as binary data.

private func processWeatherData(data: NSData) {
    do {
        if let JSON = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? NSDictionary,
           let weatherData = WeatherData.from(JSON) {
            print(weatherData.lat)
            print(weatherData.long)
            print(weatherData.windSpeed)
            print(weatherData.fahrenheit)

            for dataPoint in weatherData.hourlyDataPoints {
                print(dataPoint.fahrenheit)
            }
        }

    } catch {
        print("Unable to Initialize Weather Data")
    }
}

We cast the return value of JSONObjectWithData(_:options:) to an optional NSDictionary object and pass it to the from(_:) method of WeatherData. Mapper's syntax for initializing a Mappable instance is cleaner in my opinion. The from(_:) method is invoked on the model type, WeatherData in this example.

// Mapper
let weatherData = WeatherData.from(JSON)

// Unbox
let weatherData: WeatherData? = Unbox(data)

What I like about Unbox is that the Unbox(_:) method is throwing. Why is that important? Instead of returning an optional, you can inspect the error that is thrown. Mapper's from(_:) method always returns an optional.

Support for Enumerations

While I didn't cover enumerations in the tutorial about Unbox, both libraries provide the option to map raw values to enumeration types.

The main difference is that Unbox requires the enumeration to conform to the UnboxableEnum protocol. Mapper is smart enough to figure out how to map the raw value to the corresponding enumeration type.

Transformations

It shouldn't surprise you that both Unbox and Mapper support custom transformations. Mapper's implementation for value transformations is more flexible because you can optionally specify the transformer to use in the initializer.

That said, you can accomplish a similar result by using Unbox' performCustomUnboxingWithDictionary(_:context:closure:). This method gives the developer complete control over the unboxing process.

Conclusion

It is clear that both Unbox and Mapper are beautiful and elegant solutions for deserializing JSON data. Both libraries make it easy to convert data to model objects and each library has its pros and cons.

If I had to choose between Unbox and Mapper, I would choose for Unbox. Unbox is a bit more powerful and I especially enjoy not having to deal with deserializing binary data. I can see why the Lyft team has chosen to omit this capability from Mapper, but I like the fact that Unbox takes care of that task.

Which library would you choose? Are you currently using a similar solution for converting JSON data to model objects? Questions? Leave them in the comments below or reach out to me on Twitter. You can download the source files of the tutorial from GitHub.