Struct Codable Alamofire Generics

I wrote about Swift Generics in Protocols and Enums half a year ago to solve the problem of converting network response data to generic types.

After Swift 4, JSONSerialization method is a bit outdated.

When updating, I found a better solution.

Tools

Assume we have a JSON structure:

1
2
3
4
5
6
7
{
"id": 0,
"List": [0, 1, 2],
"C": {
"id": 0
}
}

After pasting it to Quicktype with selecting Alamofire extensions, you can get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import Foundation
import Alamofire

struct A: Codable {
let id: Int
let list: [Int]? // optional, in case some date has no list
let c: C

enum CodingKeys: String, CodingKey {
case id
case list = "List"
case c = "C"
}
}

struct C: Codable {
let id: Int
}

// MARK: - Alamofire response handlers

extension DataRequest {
fileprivate func decodableResponseSerializer<T: Decodable>() -> DataResponseSerializer<T> {
return DataResponseSerializer { _, response, data, error in
guard error == nil else { return .failure(error!) }

guard let data = data else {
return .failure(AFError.responseSerializationFailed(reason: .inputDataNil))
}

return Result { try JSONDecoder().decode(T.self, from: data) }
}
}

@discardableResult
fileprivate func responseDecodable<T: Decodable>(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<T>) -> Void) -> Self {
return response(queue: queue, responseSerializer: decodableResponseSerializer(), completionHandler: completionHandler)
}

@discardableResult
func responseA(queue: DispatchQueue? = nil, completionHandler: @escaping (DataResponse<A>) -> Void) -> Self {
return responseDecodable(queue: queue, completionHandler: completionHandler)
}
}

Please note that func responseA is a static type, only applicable to the API space of the JSON you paste. Therefore, we need a generic type for all APIs.

So we can delete func responseA and define a protocol MYRequest, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protocol MYRequest {
var router: String { get }
var api: String { get }

associatedtype T: Decodable // generics type
}

extension MYRequest {
var api: String { return "v1" }

var domain: String {
return "https://domain.com"
}

// T is used in DataResponse<T>
func request(completionHandler: @escaping (DataResponse<T>) -> Void) {
let url = "\(domain)/\(api)/\(router)"
Alamofire.request(url).responseDecodable(completionHandler: completionHandler)
}
}

Now, we can implement different requests as needed:

1
2
3
4
5
6
7
8
9
struct ARequest: MYRequest {
let router = "a"
typealias T = A
}

struct BRequest: MYRequest {
let router = "b"
typealias T = B
}

Additionally, we can define another protocol V2 with the same protocol var api: String { get } in MYRequest if we have a new CRequest and the API is upgraded to v2:

1
2
3
4
5
6
7
8
9
10
11
protocol V2 {
var api: String { get }
}
extension V2 {
var api: String { return "v2" }
}

struct C: Request, V2 {
let router = "c"
typealias T = C
}

Finally, the requests in the ViewController will look like this:

1
2
3
4
5
6
7
8
9
10
ARequest().request { [weak self] response in
if let a = response.result.value { // a is A, not Any

}
}
BRequest().request { [weak self] response in
if let b = response.result.value { // b is B, not Any

}
}

Translated by gpt-3.5-turbo