In the previous tutorial, we implemented two solutions to parse the JSON data we receive from the Dark Sky API using Swift 3. The solutions we implemented work, but they are far from perfect.

Even though we won't be writing a fully fledged JSON library, I do want to improve the second solution we created in the previous tutorial. To follow along, clone or download the playground from GitHub.

What We Want to Accomplish

Before we start making modifications to the playground, I would like to show you what result we are aiming for. This is what the extension of the WeatherData structure currently looks like.

extension WeatherData: JSONDecodable {

    public init?(JSON: Any) {
        guard let JSON = JSON as? [String: AnyObject] else { return nil }

        guard let lat = JSON["latitude"] as? Double else { return nil }
        guard let long = JSON["longitude"] as? Double else { return nil }
        guard let hourlyData = JSON["hourly"]?["data"] as? [[String: AnyObject]] else { return nil }

        self.lat = lat
        self.long = long

        var buffer = [WeatherHourData]()

        for hourlyDataPoint in hourlyData {
            if let weatherHourData = WeatherHourData(JSON: hourlyDataPoint) {
                buffer.append(weatherHourData)
            }
        }

        self.hourData = buffer
    }

}

There are several issues I would like to resolve:

  • the model is burdened with extracting the data from the JSON data
  • the initializer is failable and includes several guard statements
  • initializing the array of WeatherHourData instances is too complex and not elegant

This is the result we are aiming for.

extension WeatherData: JSONDecodable {

    public init(decoder: JSONDecoder) throws {
        self.lat = try decoder.decode(key: "latitude")
        self.long = try decoder.decode(key: "longitude")
        self.hourData = try decoder.decode(key: "hourly.data")
    }

}

This looks much better. The initializer is throwing, which means that we receive an error when something goes wrong during initialization. The most important change is that the initializer is only responsible for mapping values of the JSON data to properties of the model. That is what we are after.

Refactoring the JSONDecodable Protocol

The first change we need to make to the JSONDecodable protocol is obvious, updating the required initializer.

protocol JSONDecodable {

    init(decoder: JSONDecoder) throws

}

Instead of passing in the JSON data to the initializer, we pass in an object that is specialized in decoding the JSON data, an instance of JSONDecoder. As I showed you a moment ago, in the initializer of the JSONDecodable protocol, the model object is expected to ask the JSON decoder for the values of the keys it is interested in.

And remember that the initializer is throwing, which brings us to the second change, adding error handling.

Error Handling

Error handling in Swift if great. To add error handling, we only need to create an enumeration that conforms to the Error protocol. The Error protocol is empty. It simply tells the compiler that the conforming type can be used for error handling.

public enum JSONDecoderError: Error {
    case invalidData
    case keyNotFound(String)
    case keyPathNotFound(String)
}

The JSONDecoderError enum currently supports three cases:

  • invalid data
  • a missing key
  • a missing key path

Note that the keyNotFound and keyPathNotFound cases have an associated value, which we will use to store the key or key path that wasn't found by the JSON decoder. Associated values are fantastic.

Implementing the JSONDecoder Structure

Initialization

This brings us to the JSONDecoder structure. This is the entity that does the heavy lifting of decoding the JSON data. The public initializer is pretty simple.

import Foundation

public struct JSONDecoder {

    typealias JSON = [String: AnyObject]

    // MARK: - Properties

    private let JSONData: JSON

    // MARK: - Initialization

    public init(data: Data) throws {
        if let JSONData = try JSONSerialization.jsonObject(with: data, options: []) as? JSON {
            self.JSONData = JSONData
        } else {
            throw JSONDecoderError.invalidData
        }
    }

}

The initializer requires an explanation. Because we make use of the JSONSerialization class, we need to import the Foundation framework.

import Foundation

We also declare a type alias, JSON, for the type [String: AnyObject]. Why? This makes the code cleaner and more readable. You will start to see the benefits of this addition in a few moments.

typealias JSON = [String: AnyObject]

We declare a private constant property that stores the deserialized JSON data. This property is set in the initializer.

// MARK: - Properties

private let JSONData: JSON

The initializer of the JSONDecoder structure deserializes the Data object, which is passed in as an argument. If the Data object cannot be deserialized by the JSONSerialization class or is not of type [String: AnyObject], we throw an error, JSONDecoderError.invalidData.

// MARK: - Initialization

public init(data: Data) throws {
    if let JSONData = try JSONSerialization.jsonObject(with: data, options: []) as? JSON {
        self.JSONData = JSONData
    } else {
        throw JSONDecoderError.invalidData
    }
}

Decoding

Earlier in this tutorial, I showed you an example of what we are aiming for. In that example, we asked the JSONDecoder instance for the value for a particular key by invoking the decode(key:) method. As you can see below, this method is throwing.

extension WeatherData: JSONDecodable {

    public init(decoder: JSONDecoder) throws {
        self.lat = try decoder.decode(key: "latitude")
        self.long = try decoder.decode(key: "longitude")
        self.hourData = try decoder.decode(key: "hourly.data")
    }

}

As you probably know, Swift is very strict about types. Based on the above initializer, you would think we need to implement a decode(key:) method for every possible return type. Fortunately, generics can help us with this problem. This is what the signature of the decode(key:) method looks like.

func decode<T>(key: String) throws -> T {

}

T is a placeholder type that we use in the body of the function. Swift's type inference is what does the magic and heavy lifting. If you look at the implementation of the decode(key:) method, the puzzle starts to come together. The implementation is surprisingly simple.

func decode<T>(key: String) throws -> T {
    guard let value: T = try? value(forKey: key) else { throw JSONDecoderError.keyNotFound(key) }
    return value
}

In a guard statement, we invoke a helper method, value(forKey:), and assign the result to the value constant, which is of type T. If this operation is unsuccessful, we throw an error, JSONDecoderError.keyNotFound.

Let us continue with exploring the implementation of the value(forKey:) method. This is very similar to the decode(key:) method. The reason for moving the extraction of the value from the JSON data into a separate method becomes clear in a few moments.

private func value<T>(forKey key: String) throws -> T {
    guard let value = JSONData[key] as? T else { throw JSONDecoderError.keyNotFound(key) }
    return value
}

We have two more problems to solve. How does the JSONDecoder handle an array of model objects that is nested in another model object? And how can we add support for key paths?

Supporting Key Paths

Adding support for key paths is easier than you would think. We first need to update the decode(key:) method. If the key parameter contains a dot, we can assume we are dealing with a key path. In that case, we invoke another helper method, value(forKeyPath:).

func decode<T>(key: String) throws -> T {
    if key.contains(".") {
        return try value(forKeyPath: key)
    }

    guard let value: T = try? value(forKey: key) else { throw JSONDecoderError.keyNotFound(key) }
    return value
}

The implementation of value(forKeyPath:) can look a bit daunting at first. Let me walk you through it.

private func value<T>(forKeyPath keyPath: String) throws -> T {
    var partial = JSONData
    let keys = keyPath.components(separatedBy: ".")

    for i in 0..<keys.count {
        if i < keys.count - 1 {
            if let partialJSONData = JSONData[keys[i]] as? JSON {
                partial = partialJSONData
            } else {
                throw JSONDecoderError.invalidData
            }

        } else {
            return try JSONDecoder(JSONData: partial).value(forKey: keys[i])
        }
    }

    throw JSONDecoderError.keyPathNotFound(keyPath)
}

We store the value of JSONData in a temporary variable, partial, and we extract the keys from the key path.

var partial = JSONData
let keys = keyPath.components(separatedBy: ".")

Next, we loop through the array of keys and, as long as the current key is not the last key, we extract the JSON data that corresponds with the current key, storing the value in partial.

The moment we have a reference to the last key in keys, we instantiate an instance of JSONDecoder and use the value(forKey:) method to ask it for the value for that key.

for i in 0..<keys.count {
    if i < keys.count - 1 {
        if let partialJSONData = JSONData[keys[i]] as? JSON {
            partial = partialJSONData
        } else {
            throw JSONDecoderError.invalidData
        }

    } else {
        return try JSONDecoder(JSONData: partial).value(forKey: keys[i])
    }
}

Notice that we use a different initializer to initialize the JSONDecoder instance.

private init(JSONData: JSON) {
    self.JSONData = JSONData
}

The value(forKeyPath:) is also throwing, which means we throw an error whenever something goes haywire.

Arrays of Model Objects

To add support for arrays of model objects, we need to implement a variation of the decode(key:) method. The signature and implementation of this method are almost identical. The difference is the return type, [T], and the type of the value constant in the guard statement. Note that the placeholder type, T, is required to conform to the JSONDecodable protocol. Why that is becomes clear in a moment.

func decode<T: JSONDecodable>(key: String) throws -> [T] {
    if key.contains(".") {
        return try value(forKeyPath: key)
    }

    guard let value: [T] = try? value(forKey: key) else { throw JSONDecoderError.keyNotFound(key) }
    return value
}

The implementation of the second decode(key:) method implies that we also need to implement a second value(forKey:) method and a second value(forKeyPath) method.

In the value(forKey:) method, we first extract the array of JSON data we are interested in from the JSONData property. Next, we use the map(_:) method to create an instance of T for every element in the array of JSON data. That is why the placeholder type, T, needs to conform to the JSONDecodable protocol.

private func value<T: JSONDecodable>(forKey key: String) throws -> [T] {
    if let value = JSONData[key] as? [JSON] {
        return try value.map({ (partial) -> T in
            let decoder = JSONDecoder(JSONData: partial)
            return try T(decoder: decoder)
        })
    }

    throw JSONDecoderError.keyNotFound(key)
}

The implementation of value(forKeyPath:) is almost identical to that of the one we implemented earlier. The only difference is the method's return type.

private func value<T: JSONDecodable>(forKeyPath keyPath: String) throws -> [T] {
    var partial = JSONData
    let keys = keyPath.components(separatedBy: ".")

    for i in 0..<keys.count {
        if i < keys.count - 1 {
            if let partialJSONData = JSONData[keys[i]] as? JSON {
                partial = partialJSONData
            } else {
                throw JSONDecoderError.invalidData
            }

        } else {
            return try JSONDecoder(JSONData: partial).value(forKey: keys[i])
        }
    }

    throw JSONDecoderError.keyPathNotFound(keyPath)
}

Refactoring WeatherData and WeatherHourData

We can now refactor WeatherData and WeatherHourData. This is what their implementations look like. Note that only the extension for conforming to the JSONDecodable protocol is shown.

extension WeatherData: JSONDecodable {

    public init(decoder: JSONDecoder) throws {
        self.lat = try decoder.decode(key: "latitude")
        self.long = try decoder.decode(key: "longitude")
        self.hourData = try decoder.decode(key: "hourly.data")
    }

}
extension WeatherHourData: JSONDecodable {

    public init(decoder: JSONDecoder) throws {
        self.windSpeed = try decoder.decode(key: "windSpeed")
        self.temperature = try decoder.decode(key: "temperature")
        self.precipitation = try decoder.decode(key: "precipIntensity")

        let time: Double = try decoder.decode(key: "time")
        self.time = Date(timeIntervalSince1970: time)
    }

}

We currently don't have support for extracting and transforming values to Date objects, which is why this is handled in the initializer of the WeatherHourData structure. This is a limitation of the current implementation of the JSONDecoder structure.

Updating the Playground

The playground can be simplified as well. Take a look at what we have thanks to the JSONDecodable protocol.

import Foundation

// Fetch URL
let url = Bundle.main.url(forResource: "response", withExtension: "json")!

// Load Data
let data = try! Data(contentsOf: url)

do {
    let decoder = try JSONDecoder(data: data)
    let weatherData = try WeatherData(decoder: decoder)

    print(weatherData)

} catch {
    print(error)
}

Cherry on the Cake

We can make the syntax even better by adding a static method to the JSONDecoder class and, once again, leveraging Swift's type inference. This is what we want to accomplish.

let weatherData: WeatherData = try JSONDecoder.decode(data: data)

We can do this by implementing a static method that does the decoding for us. Add the following method to the JSONDecoder structure.

// MARK: - Static Methods

public static func decode<T: JSONDecodable>(data: Data) throws -> T {
    let decoder = try JSONDecoder(data: data)
    return try T(decoder: decoder)
}

The implementation is simple thanks to generics and type inference. This should look familiar by now.

Because we are working in a playground, we need to make the JSONDecodable protocol public. Remember that additional source files of a playground are put in a separate module and not directly accessible by the playground.

public protocol JSONDecodable {

    init(decoder: JSONDecoder) throws

}

This addition was inspired by the Unbox library, developed by John Sundell. Unbox is a fantastic, lightweight library for parsing JSON data.

Are We Done Yet

Is this the next JSON library that everyone is going to use. I doubt it. It has several shortcomings and its feature set is quite limited compared to other options.

But we have accomplished a significant goal. We have parsed JSON data without using a third party solution. You learned how to parse JSON data and you also learned how protocols and generics can help you with that.

You can download the playground from GitHub.