Protocol-Based Generic Networking -Part 2 JSONEncoder and Encodable for Post request in Swift.

James Rochabrun
7 min readOct 22, 2018

--

Its been a while since I wrote “Protocol Based Generic Networking using JSONDecoder and Decodable in Swift 4” on that post I talked about how to use Decodable, protocols and generics to create a reusable networking layer.

It worked great but the API was limited to the GET HTTP method, that's why on this tutorial I want to extend it to use the POST method, I will do my best to show you how to use enums to do the job, I will assume that you didn’t read part 1 so I will start from zero in a new project and you can always go back to the first part for more details.

We will start with the GenericAPI class, create a new file and copy and paste…

protocol GenericAPIClient {    var session: URLSession { get }    func fetch<T: Decodable>(with request: URLRequest, decode: @escaping (Decodable) -> T?, completion: @escaping (Result<T, APIError>) -> Void)}extension GenericAPIClient {    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(description: error?.localizedDescription ?? "No description"))                return            }            guard httpResponse.statusCode == 200  else {                completion(nil, .responseUnsuccessful(description: "\(httpResponse.statusCode)"))                return            }            guard let data = data else { completion(nil, .invalidData); return }            do {                let genericModel = try JSONDecoder().decode(decodingType, from: data)                completion(genericModel, nil)            } catch let err {                completion(nil, .jsonConversionFailure(description: "\(err.localizedDescription)"))            }        }        return task    }    /// success respone executed on main thread.    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            DispatchQueue.main.async {                guard let json = json else {                    error != nil ? completion(.failure(.decodingTaskFailure(description: "\(String(describing: error))"))) : completion(.failure(.invalidData))                    return                }                guard let value = decode(json) else { completion(.failure(.jsonDecodingFailure)); return }                completion(.success(value))            }        }        task.resume()    }}

This protocol extension contains all the code needed to create a task that retrieves the contents of a URL based on the specified URL request object, also instead of serializing the response, it decodes it using Decodable protocol (again I suggest going to part 1 for more details).

Now you should have a few compile errors and that's because you will need to create a few classes, let's start with the APIError, create a new file call it APIError copy and paste…

enum APIError: Error {    case invalidData    case jsonDecodingFailure    case responseUnsuccessful(description: String)    case decodingTaskFailure(description: String)    case requestFailed(description: String)    case jsonConversionFailure(description: String)    case postParametersEncodingFalure(description: String)    var customDescription: String {        switch self {        case .requestFailed(let description): return "APIError - Request Failed -> \(description)"        case .invalidData: return "Invalid Data"        case .responseUnsuccessful(let description): return "APIError - Response Unsuccessful status code -> \(description)"        case .jsonDecodingFailure: return "APIError - JSON decoding Failure"        case .jsonConversionFailure(let description): return "APIError - JSON Conversion Failure -> \(description)"        case .decodingTaskFailure(let description): return "APIError - decodingtask failure with error -> \(description)"        case .postParametersEncodingFalure(let description): return "APIError - post parameters failure -> \(description)"        }    }}

Here we start using enums with associated types to construct a specific type of error, now let’s construct another enum, this time it will hold two cases, one for a successful request and one for a failure case, create a new file and name it Result…

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

If you are not familiar with this syntax I suggest to read about generics on Swift there are great tutorials online for starters but if you are familiar already with generics and want to see a more advanced usage check this post out.

Ok, now let's create a model for our HTTP methods, create a new file and call it HTTPMethod, there are others HTTP methods than “GET” and “POST” but this tutorial is focused on showing you how to use Encodable to use “POST” so will keep this model simple…

enum HTTPMethod: String {    case post = "POST"    case get = "GET"}

As you know you will need to pass some type of headers to your request in a POST method so let’s create a model that handles that, again new file…

enum HTTPHeader {    case contentType(String)    case accept(String)    case authorization(String)    var header: (field: String, value: String) {        switch self {        case .contentType(let value): return (field: "Content-Type", value: value)        case .accept(let value): return (field: "Accept", value: value)        case .authorization(let value): return (field: "Authorization", value: value)        }    }}

For a full list of HTTP headers check this link you can extend this enum with the full list!

Ok, now we need an endpoint, I really liked the approach of using an implementation that I saw on Treehouse, handling strings with different endpoints can become easily a mess so by using this protocol extension we, later, use an enum to manage all those endpoints in one place, create a new file and call it Endpoint….

protocol Endpoint {    var base:  String { get }    var path: String { get }}extension Endpoint {    var urlComponents: URLComponents? {        guard var components = URLComponents(string: base) else { return nil }        components.path = path        return components    }    var request: URLRequest? {        guard let url = urlComponents?.url ?? URL(string: "\(self.base)\(self.path)") else { return nil }        let request = URLRequest(url: url)        return request    }}

Here, we just construct an endpoint with a base path and the path to direct the request, if you already saw part 1 I apologize that most of the content so far comes from that tutorial but here is the good part, lets add a function that will construct a POST request, inside this protocol extension add this function…

func postRequest<T: Encodable>(parameters: T, headers: [HTTPHeader]) -> URLRequest? {        guard var request = self.request else { return nil }        request.httpMethod = HTTPMethod.post.rawValue        do {            request.httpBody = try JSONEncoder().encode(parameters)        } catch let error {            print(APIError.postParametersEncodingFalure(description: "\(error)").customDescription)            return nil        }        headers.forEach { request.addValue($0.header.value, forHTTPHeaderField: $0.header.field) }        return request    }

Oh man I love generics, protocols and enums, they really help reduce code duplication, let me show you why, the parameters parameter in the function signature is a generic type that conforms to Encodable, so it can accept any type of model as long as it conforms to this protocol, this way we won't need to create a new different function for each different model, the headers parameter its an array of HTTPHeader objects which is the enum that we create a few moments ago.

Inside the function we check if there is a request then we use the HTTPMethod enum case to define the httpMethod in the request, later we use JSONEncoder to encode the model, and finally we perform a for each to add the HTTPField and its corresponding value to the request headers, all done! this is all that we need, so how you will use it? well I apologize but for simplicity I won't use any API to perform a real request however I will show you how to use it step by step, let’s say we are using an API to retrieve your favorite show on Netflix, mine is Narcos (very excited for coming season 4 BTW) anyway so here is how the object that will conform to Endpoint will look like….

enum NarcosFeed {
case narcs
case police
}
extension NarcosFeed: Endpoint {

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

var path: String {
switch self {
case .narcs: return "/3/narcs"
case .police: return "/3/police"
}
}
}

You get the picture here? let's say now you want to add “politics” to your API you just need to add a new case and its corresponding path, ok let's say that our APP let the user search for characters of the show, first you will need a model that looks like this to decode the response…

struct Narc: Decodable {    let name: String}

Then lets say the API that fetches characters uses a POST request that takes as a parameter an ID that can be a name like “Pablo” it also needs the “Content-Type” key with “application/json” value for the headers, so you will need an object to encode the needed parameter, it can look like this…

struct NarcParameter: Encodable {    let id: String}

Now, you are going to need to create a Client that conforms to GenericAPIClient protocol and inside it create all the functions needed for the data that you need for your app, it will look like this…

class NarcosClient: GenericAPIClient {    internal let session: URLSession    init(configuration: URLSessionConfiguration) {        self.session = URLSession(configuration: configuration)    }    convenience init() {        self.init(configuration: .default)    }    /// Fetch Narcs Characters by name    func fetchNarcs(id: String, completion: @escaping (Result<Narc?, APIError>) -> ()) {        let parameter = NarcParameter(id: id)        guard let request = NarcosFeed.narcs.postRequest(parameters: parameter,                                                                     headers: [HTTPHeader.contentType("application/json")]) else { return }        fetch(with: request , decode: { json -> Narc? in            guard let results = json as? Narc else { return  nil }            return results        }, completion: completion)    }}

Inside the fetchNarcs function, you can see how we use all the components written before, the request is constructed using the corresponding path “/narcs” and then the function in Endpoint protocol takes the parameters and headers.

Finally, this is how a typical call to your NarcosClient will look like…

NarcosClient().fetchNarcs(id: "Pablo") { result in    switch result {    case .success(let narc):        print("plata o plomo...\(String(describing: narc))")    case .failure(let error):        print("The error \(error)")    }}

Now you can see how the Result enum can easily help us determine the state of the request and act accordingly.

I hope you find this helpful, and again if you already read part 1 I apologize for the duplicate content but it really helped me explain how to create a POST request following the same approach on the first part, you can find the full example for this tutorial on this repo.

Thank you!

--

--