Protocol Based Generic Networking using JSONDecoder and Decodable in Swift 4

Swift 4 has been out for a while and XCode 9.2 has been released during this week (December 2017), no many dramatic updates for Swift 4 but a few new tools that we can use to write cleaner and reusable code, in this post we are going to mainly focus on JSONDecoder and the Decodable protocol that helps with making very easy to parse JSON.

We are going to create a generic API that you can reutilize for any model, because what I want to highlight on this project is how to create the networking layer we will only going to parse the JSON and print it as models in the console. As a bonus, I will add a second part to this project where we will use protocol extensions to create the UI using XIBS.

For this project, we will use the API from “The Movie DB” you can check its documentation here.

Let’s start by downloading this starter project, on it you will find a group called Model and an empty one called Networking. The model one contains two models one called MovieFeedResult that has a property of type array of Movies, and one called Movie, we will revisit these files after we complete the networking layer.

We will create the Networking layer step by step, create a new Swift file and call it Result and copy and paste this enum into it…

enum Result<T, U> where U: Error  {
case success(T)
case failure(U)
}

When we make an URL request we may get two different kinds of response, either a successful one or one that failed. Usually, we will pass both in a completion handler setting to nil one of them, however with this Generic enum we can avoid that and pass just what we need for each case.

Next, we will create a protocol with a protocol extension, create a new file and call it Endpoint, copy and paste…

protocol Endpoint {
var base: String { get }
var path: String { get }
}
extension Endpoint {
var apiKey: String {
return "api_key=34a92f7d77a168fdcd9a46ee1863edf1"
}

var urlComponents: URLComponents {
var components = URLComponents(string: base)!
components.path = path
components.query = apiKey
return components
}

var request: URLRequest {
let url = urlComponents.url!
return URLRequest(url: url)
}
}

This protocol has two required properties called “base” and “path” just like a regular endpoint, it also has a few computed properties in an extension, one is the APIKey required to be able to make the request (I got this API key from the Movie DB website) it also has a urlComponents property that will construct the url and finally the request that returns an URLRequest.

The movies API can return a different feed of movies, like movies that are been playing now or the top rated ones, we are going to create an enum that will be in charge of managing the different types of feed, create a new empty file and call it MovieFeed…

enum MovieFeed {
case nowPlaying
case topRated
}
extension MovieFeed: Endpoint {

var base: String {
return "https://api.themoviedb.org"
}

var path: String {
switch self {
case .nowPlaying: return "/3/movie/now_playing"
case .topRated: return "/3/movie/top_rated"
}
}
}

Here we are making MovieFeed conform to Endpoint so it will be required to provide two pieces of information, the base path and the path associated with the corresponding case, this way we will construct the endpoint for each type of feed.

If an error occurs in an URL request it can be for many reasons and provide detailed information about it, can be very useful for us as developers but also for users, we are going to create an enum that will hold multiple types of errors and will return a description for each of them, create a file and call it APIError…

enum APIError: Error {
case requestFailed
case jsonConversionFailure
case invalidData
case responseUnsuccessful
case jsonParsingFailure
    var localizedDescription: String {
switch self {
case .requestFailed: return "Request Failed"
case .invalidData: return "Invalid Data"
case .responseUnsuccessful: return "Response Unsuccessful"
case .jsonParsingFailure: return "JSON Parsing Failure"
case .jsonConversionFailure: return "JSON Conversion Failure"
}
}
}

Here is where things become interesting, we are going to use JSONDecoder and the Decodable protocol to create a generic APICLient that you can reuse in any project and with any type of object or collection of them, create a new empty project and call it APIClient, let’s start by creating the protocol…

protocol APIClient {
var session: URLSession { get }
func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Decodable) -> T?, completion: @escaping (Result<T, APIError>) -> Void)
}

Every object that conforms to APIClient will have a session and will be able to use the generic fetch function, now let’s add some functionality to this protocol in an extension…

extension APIClient {
    typealias JSONTaskCompletionHandler = (Decodable?, APIError?) -> Void
private func decodingTask<T: Decodable>(with request: URLRequest, decodingType: T.Type, completionHandler completion: @escaping JSONTaskCompletionHandler) -> URLSessionDataTask {

let task = session.dataTask(with: request) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
completion(nil, .requestFailed)
return
}
if httpResponse.statusCode == 200 {
if let data = data {
do {
let genericModel = try JSONDecoder().decode(decodingType, from: data)
completion(genericModel, nil)
} catch {
completion(nil, .jsonConversionFailure)
}
} else {
completion(nil, .invalidData)
}
} else {
completion(nil, .responseUnsuccessful)
}
}
return task
}
}

This function will be the one in charge of parsing or rather decoding the JSON data, it takes a request as a parameter, the type of an object that conforms to Decodable and a completion handler, it finally returns an URLSessionDataTask. You can see that inside of it we just check the response and statusCode and based on that we Decode the data from the response or in case of an error provide the corresponding error information.

The cool part though is how we use JSONDecoder here to parse (or decode) any type of object or even a collection of them. By the way, see that we don’t call resume here we just want to return the task.

To finish this protocol lets add the logic to the fetch function, inside the extension copy and paste…

func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Decodable) -> T?, completion: @escaping (Result<T, APIError>) -> Void) {     
let task = decodingTask(with: request, decodingType: T.self) { (json , error) in

//MARK: change to main queue
DispatchQueue.main.async {
guard let json = json else {
if let error = error {
completion(Result.failure(error))
} else {
completion(Result.failure(.invalidData))
}
return
}
if let value = decode(json) {
completion(.success(value))
} else {
completion(.failure(.jsonParsingFailure))
}
}
}
task.resume()
}

Inside this function, we return a task from the helper method that we just wrote and passing as a parameter for decodingType the type that this function will decode, after checking if the JSON its not nil and if has been decoded, we pass the decoded data in the success case of the completion handler.

One last piece and we will be ready to go, now that we have a generic API that will fetch and decode any type of objects lets create a client for our Movies app, it will conform to APIClient to get its functionality, create a new file and call it MovieClient…

class MovieClient: APIClient {
    let session: URLSession

init(configuration: URLSessionConfiguration) {
self.session = URLSession(configuration: configuration)
}

convenience init() {
self.init(configuration: .default)
}

//in the signature of the function in the success case we define the Class type thats is the generic one in the API
func getFeed(from movieFeedType: MovieFeed, completion: @escaping (Result<MovieFeedResult?, APIError>) -> Void) {
fetch(with: movieFeedType.request , decode: { json -> MovieFeedResult? in
guard let movieFeedResult = json as? MovieFeedResult else { return nil }
return movieFeedResult
}, completion: completion)
}
}

This has a convenient init because for this purposes we want always to set the URLSession to default.

We are going to create a function that accepts a MovieFeed as a parameter so when we call this function it will be very clear what type of feed we are asking for, the rest is just passing the request for the corresponding MovieFeed to the generic fetch method of the APIClient protocol.

So how does this will work? well, the fetch generic function will convert the generic type to MovieFeedResult? type so when the decode completion handler is executed the json it’s now a Decodable model, then, we downcast it safely to a MovieFeedResult and return it, finally, we pass the completion, I know that this explanation can get wordy so I just suggest read the function a couple of times.

Let’s now go to the view controller file and on viewDidLoad test our function…

client.getFeed(from: .nowPlaying) { result in
            switch result {
case .success(let movieFeedResult):

guard let movieResults = movieFeedResult?.results else { return }
print(movieResults)
case .failure(let error):
print("the error \(error)")
}
}

go to the console and you should be able to see an array of movies!

Ok, now you are probably asking your self where are all the keys that we usually use when we are parsing JSON, well let’s try to find them in the model files…nothing? so where are they? what kind of witchcraft is this? how are we parsing it?!

Well there is no magic here and this is how Decodable works if you check the JSON of this response here and check for the keys you will see that in the MOVIE model its properties match exactly in how these keys are written, the rest is handled by the protocol.

In conclusion, you will need to make your model conform to Decodable and to avoid issues make your properties optional just in case one of those keys does not exist.

You can find the complete implementation of this part here, hope you find this helpful!

PS: Thanks to Treehouse and Pasan Premaratne for the protocol based idea ;)