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
switchte 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.
