← Volver al blog

Modelar respuestas de API con enum en Swift: una solución elegante para APIs impredecibles

Cuando una API devuelve HTTP 200 pero con formatos distintos según el resultado, un enum con valores asociados es la solución más limpia en Swift.

No sé si esto es más común de lo que parece, pero a veces las APIs manejan un estándar de respuestas diferente al 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.

  1. ✅ Éxito (y viene con datos)
  2. ❌ Error (con código y mensaje)
  3. 🌀 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.