Skip to content

Instantly share code, notes, and snippets.

@kittisak-phetrungnapha
Last active October 8, 2019 16:50
Show Gist options
  • Save kittisak-phetrungnapha/282f9437ba8d6c5cfefbae8d336212b8 to your computer and use it in GitHub Desktop.
Save kittisak-phetrungnapha/282f9437ba8d6c5cfefbae8d336212b8 to your computer and use it in GitHub Desktop.
CustomDecodableResponseSerializer in Alamofire 5 for automatic mapping decodable object if the response is success or throw an error object if it is fail that dynamic with your api. The explanation is in the comment.
import Foundation
import Alamofire
typealias EmptyContent = Empty
extension DataRequest {
@discardableResult
func customResponseDecodable<T: Decodable>(queue: DispatchQueue = .main,
decoder: DataDecoder = JSONDecoder(),
completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self {
return response(queue: queue,
responseSerializer: CustomDecodableResponseSerializer(decoder: decoder),
completionHandler: completionHandler)
}
}
private final class CustomDecodableResponseSerializer<T: Decodable>: ResponseSerializer {
let decoder: DataDecoder
init(decoder: DataDecoder = JSONDecoder()) {
self.decoder = decoder
}
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
guard error == nil else { throw error! }
guard let data = data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else {
throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)"))
}
return emptyValue
}
let statusCode = response?.statusCode ?? 0
let isSuccessStatusCode = (200...299).contains(statusCode)
if isSuccessStatusCode {
do {
return try decoder.decode(T.self, from: data)
} catch {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
}
} else {
let errorDict = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any]
let message = (errorDict?["message"] as? String) ?? "Your default error message"
let customError = CustomError.apiError(message: message)
throw AFError.responseSerializationFailed(reason: .customSerializationFailed(error: customError))
}
}
}
import Foundation
import Alamofire
protocol NetworkSession: class {
func request<T: Decodable>(router: Router, decoder: JSONDecoder, completion: @escaping (Result<T, Error>) -> Void)
}
final class APIManager: NetworkSession {
static let shared = APIManager()
private init() {}
func request<T: Decodable>(router: Router,
decoder: JSONDecoder = JSONDecoder(),
completion: @escaping (Result<T, Error>) -> Void) {
AF.request(router).customResponseDecodable(decoder: decoder) { (response: DataResponse<T, AFError>) in
switch response.result {
case .success(let value):
completion(.success(value))
case .failure(let afError):
if case .responseSerializationFailed(.customSerializationFailed(let customError)) = afError {
completion(.failure(customError))
} else {
completion(.failure(afError))
}
}
}
}
}
import Foundation
enum CustomError: LocalizedError {
case apiError(message: String)
var errorDescription: String? {
switch self {
case .apiError(let message):
return message
}
}
}
import Foundation
import Alamofire
enum Router: URLRequestConvertible {
// MARK: - Endpoints
case createUser(parameters: Parameters)
case readUser(username: String)
case updateUser(username: String, parameters: Parameters)
case destroyUser(username: String)
private static let baseURLString = "Your base URL"
// MARK: - HTTPMethod
private var method: HTTPMethod {
switch self {
case .createUser:
return .post
case .readUser:
return .get
case .updateUser:
return .put
case .destroyUser:
return .delete
}
}
// MARK: - Path
private var path: String {
switch self {
case .createUser:
return "/users"
case .readUser(let username):
return "/users/\(username)"
case .updateUser(let username, _):
return "/users/\(username)"
case .destroyUser(let username):
return "/users/\(username)"
}
}
// MARK: - URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let url = try Self.baseURLString.asURL()
var urlRequest = URLRequest(url: url.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue
switch self {
case .createUser(let parameters):
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
case .updateUser(_, let parameters):
urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)
case .readUser,
.destroyUser:
break
}
return urlRequest
}
}
// Success with response (200).
APIManager.shared.request(router: .readUser(username: "test_username")) { (result: Result<User, Error>) in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print(error.localizedDescription)
}
}
// Success without response (204, 205) aka empty content.
APIManager.shared.request(router: .readUser(username: "test_username")) { (result: Result<EmptyContent, Error>) in
switch result {
case .success:
print("Your request is success")
case .failure(let error):
print(error.localizedDescription)
}
}
// Error due to wrong username.
APIManager.shared.request(router: .readUser(username: "wrong_username")) { (result: Result<User, Error>) in
switch result {
case .success:
print(user)
case .failure(let error):
print(error.localizedDescription) // Your username or password is invalid.
}
}
@kittisak-phetrungnapha
Copy link
Author

kittisak-phetrungnapha commented Oct 8, 2019

Alamofire 5 provides us the function for request data from remote then map to any object that conform to Decodable which is great below.

public func responseDecodable<T: Decodable>(of type: T.Type = T.self,
                                                queue: DispatchQueue = .main,
                                                decoder: DataDecoder = JSONDecoder(),
                                                completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self { ... }

However, in the real world your request does not always success. Sometime it is error from some reasons. So, your api return json structure for representing the error message that different from success case json structure. If you use the above build-in function you will end up with mapping decodable object error which is make sense because the json structure is different. That why I create this gist to solve the problem. Let say you want to fetch the user data related with input username so this below is response from api in case of the request is success.

{
"first_name": "Alice", 
"last_name": "Bob",
"job": "engineer"
}

And in case of our request is fail due to wrong username so the api return us back like this

{
"message": "Your username or password is invalid"
}

Which this function it can handle both cases without any concern (at the moment of writing this).

func customResponseDecodable<T: Decodable>(queue: DispatchQueue = .main,
                                               decoder: DataDecoder = JSONDecoder(),
                                               completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self { ... }

Enjoy coding!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment