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

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.

  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.


Deja un comentario