Last active
October 8, 2019 16:50
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum CustomError: LocalizedError { | |
case apiError(message: String) | |
var errorDescription: String? { | |
switch self { | |
case .apiError(let message): | |
return message | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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. | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Alamofire 5 provides us the function for request data from remote then map to any object that conform to
Decodable
which is great below.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.
And in case of our request is fail due to wrong username so the api return us back like this
Which this function it can handle both cases without any concern (at the moment of writing this).
Enjoy coding!