No se si esto es mas común de lo que parece, pero a veces las APIs manejan un estándar de respuestas diferentes a las que comúnmente esperamos. Te devuelven HTTP 200
, pero la respuesta tiene distintos formatos dependiendo de si todo salió bien o si todo salió mal. Para eso, Swift tiene una opción que podemos aprovechar: enum
con valores asociados.
- ✅ Éxito (y viene con datos)
- ❌ Error (con código y mensaje)
- 🌀 Respuesta rara que no sabemos ni cómo decodificar
En este post te muestro cómo crear un solo Codable
que representa los tres posibles escenarios que te puede devolver una API:
Respuesta esperada:
{ "code": 0, "message": "OK", "data": { "id": 42, "name": "Ada Lovelace" } }
Errores específicos de la API:
{ "code": 401, "message": "Invalid API key" }
Mensajes no esperados:
{ "unexpected_field": "who knows?" }
🧱 El modelo unificado con enum
Creamos una única enum que pueda representar estos tres escenarios:
enum APIResult<T: Decodable>: Decodable { case success(T) case failure(Int, String) case unknown private enum CodingKeys: String, CodingKey { case code case message case data } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) if let code = try? container.decode(Int.self, forKey: .code), let message = try? container.decode(String.self, forKey: .message) { if code == 0, let data = try? container.decode(T.self, forKey: .data) { self = .success(data) } else { self = .failure(code, message) } } else { self = .unknown } } }
🎯 Este init(from:)
intenta leer el code
y message
. Si no puede, lo marca como .unknown
. Si code == 0
, intenta decodificar los datos. Si no, lo trata como un error del backend.
🧍♀️ Nuestro modelo de datos
struct User: Decodable { let id: Int let name: String }
🔌 APIClient
de ejemplo.
actor APIClient { static let shared = APIClient() private let session = URLSession.shared func request<T: Decodable>( url: URL, expecting type: T.Type ) async throws -> APIResult<T> { let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } return try JSONDecoder().decode(APIResult<T>.self, from: data) } }
🎮 Cómo usarlo
@MainActor func fetchUser() async { do { let url = URL(string: "https://api.example.com/user")! let result: APIResult<User> = try await APIClient.shared.request(url: url, expecting: User.self) switch result { case .success(let user): print("Bienvenido \(user.name)") case .failure(let code, let message): print("Error \(code): \(message)") case .unknown: print("Respuesta inesperada del servidor 🌀") } } catch { print("Error de red: \(error.localizedDescription)") } }
🧪 ¿Por qué este enfoque es útil?
- Tenés una sola estructura para manejar los distintos tipos de respuesta.
- El
switch
te fuerza a cubrir todos los casos (ideal para evitar bugs). - Podés reutilizar este patrón en cualquier endpoint que siga la misma convención.
✅ En resumen
Si tu API responde con múltiples estructuras y todas usan HTTP 200
, usar un enum
con valores asociados es una forma limpia y Swifty de manejarlo:
- 🎯
success(T)
- ❌
failure(code, message)
- 🌀
unknown
Esto te permite tener un solo punto de entrada, sin tener que duplicar modelos ni hacer lógica repetida.