Swift y Objective-C

[Swift intermedio #6] Codable avanzado en Swift: guía para cuatro trampas

Desde lo que genera realmente la síntesis automática de Codable hasta las soluciones estándar para claves incompatibles, fechas, estructuras anidadas y fallos parciales, además de depurar DecodingError.

6 min de lectura
Imagen de portada de [Swift intermedio #6] Codable avanzado en Swift: guía para cuatro trampas

Codable parece magia al principio. Añades : Codable cinco letras a un struct y la conversión JSON aparece gratis. Pero la magia desaparece al conectar una API real: el servidor usa snake_case y Swift camelCase, los formatos de fecha varían según la API, y un solo elemento defectuoso puede hacer fallar la decodificación completa y dejar la pantalla vacía.

La sexta entrega de la serie intermedia profundiza en Codable: qué genera realmente la síntesis automática y las soluciones estándar para cuatro situaciones inevitables en producción: claves, fechas, anidamiento y fallos parciales.

La esencia de la magia — código que escribe el compilador

Codable es el typealias de Encodable & Decodable y de los protocolos que exigen saber cómo escribirse en un codificador y cómo crearse desde un decodificador. La apariencia mágica se debe a la síntesis automática del compilador. Si todas las propiedades almacenadas son Codable, el compilador genera dos cosas.

Primero, un enum CodingKeys: una lista de claves cuyos casos coinciden con los nombres de las propiedades. Segundo, las implementaciones de init(from:) y encode(to:), que leen y escriben cada propiedad mediante esas claves. Personalizar Codable consiste, por tanto, en decidir cuál de esos dos resultados reemplazar manualmente. Si solo cambian las claves, CodingKeys; si cambia la estructura, también init(from:). Con este esquema, todo se reduce a cuánto código escribir a mano.

Otro dato importante: Codable no es exclusivo de JSON. Como los codificadores y decodificadores se pueden sustituir, puedes usar PropertyListEncoder para plist o librerías de terceros para XML y YAML. El tipo solo conoce cómo representarse; el codificador decide el formato. Es separación de responsabilidades.

Situación 1. Nombres de clave distintos — CodingKeys y estrategias de claves

Si el servidor envía user_name pero quieres llamar userName a la propiedad, hay dos niveles de solución.

Si la regla es coherente en todo el sistema, basta una línea de configuración del decodificador: decoder.keyDecodingStrategy = .convertFromSnakeCase. Todas las claves se convierten automáticamente de snake_case a camelCase. Si toda la API sigue la convención, esta es la respuesta correcta.

Si las reglas son irregulares o quieres cambiar el nombre explícitamente, define CodingKeys de forma manual.

struct User: Codable {
    let userName: String
    let signupDate: Date

    enum CodingKeys: String, CodingKey {
        case userName = "user_nm"     // clave heredada del servidor
        case signupDate = "created"
    }
}

Recuerda un efecto secundario: si omites un caso en CodingKeys, esa propiedad queda excluida de la codificación y la decodificación. Sirve para estados exclusivos del dispositivo, como indicadores de caché, pero las propiedades excluidas deben tener un valor predeterminado.

Diagrama de una canalización JSON con cuatro zonas: snake case, fechas, anidamiento y fallos parciales
Las cuatro grandes trampas de Codable en producción: claves, fechas, anidamiento y fallos parciales

Situación 2. Fechas — un campo minado de formatos

Date es la trampa más frecuente de Codable en producción. JSON no tiene un estándar único para fechas, así que cada servidor puede usar marcas de tiempo Unix (1720000000), ISO 8601 (“2026-07-15T09:30:00Z”) o formatos personalizados (“2026-07-15 09:30”).

La solución es ajustar dateDecodingStrategy al formato del servidor: .secondsSince1970, .iso8601 o .formatted(formatter) para un formato personalizado. DateFormatter personalizado tiene dos trampas. Si no fijas locale en en_US_POSIX, el análisis puede fallar según la configuración de 12 o 24 horas del usuario. Además, .iso8601 falla si el servidor envía ISO 8601 con milisegundos; actívalos en ISO8601DateFormatter. La mayoría de los errores misteriosos en los que solo fallan algunos usuarios se deben a estas dos trampas.

Si cada campo de una misma API usa un formato distinto, lo práctico es recibir ese campo como String y convertirlo mediante una propiedad calculada, o ramificar con la estrategia .custom.

Situación 3. Anidamiento y estructuras incompatibles — la forma del servidor frente a la mía

Ocurre cuando el JSON del servidor llega envuelto en varias capas, por ejemplo {"data": {"user": {...}}}. La solución más sencilla es crear el tipo envoltorio tal cual: reflejar la estructura del servidor con struct Envelope: Codable { let data: DataBox } y extraer el valor en el punto de llamada mediante envelope.data.user. Es explícito y fácil de depurar, por lo que suele ser la mejor base en producción.

Si quieres que tu modelo tenga otra forma, por ejemplo un User plano, implementa init(from:) manualmente y recorre la jerarquía con nestedContainer. Aumenta el código, pero deja un modelo más limpio y centrado en el dominio. El criterio es el alcance de uso: un modelo central de toda la aplicación justifica un init(from:) manual; una respuesta de una sola pantalla se resuelve mejor reflejando el envoltorio.

Situación 4. Fallos parciales — cuando un elemento hace fallar todo

Es la situación más dolorosa en producción. Si un campo obligatorio es null en una lista de 100 productos, falla la decodificación del array completo y la pantalla queda vacía. Como vimos en el capítulo sobre errores, los errores lanzados se propagan.

La primera defensa son los opcionales. Declara honestamente como let thumbnail: URL? los campos que el servidor pueda omitir. El principio de que un opcional expresa “puede no existir” se aplica directamente al diseño del modelo.

La defensa estructural es un envoltorio tolerante a fallos. Un patrón estándar consiste en crear un envoltorio genérico que convierta en nil los fallos al decodificar elementos.

struct FailableItem<T: Decodable>: Decodable {
    let value: T?
    init(from decoder: Decoder) throws {
        value = try? T(from: decoder)   // nil si falla nil
    }
}

let items = try decoder.decode([FailableItem<Product>].self, from: data)
    .compactMap(\.value)   // conservar solo los que tienen éxito

Es la combinación de try? y compactMap, las dos herramientas vistas en entregas anteriores. Expresa como tipo la política de “descartar un elemento defectuoso y conservar el resto”. Atención: este patrón oculta los fallos silenciosamente. En producción, registra el número de fallos para que los problemas de datos del servidor no queden ocultos.

Ilustración de una máquina clasificadora que filtra un elemento defectuoso a un contenedor nil y deja pasar los demás
Descarta un elemento defectuoso y conserva el resto, pero registra lo descartado

Depuración — DecodingError ya contiene la respuesta

Por último, cómo investigar un fallo de decodificación. Al capturar try decoder.decode(...) obtienes DecodingError, que es más útil de lo esperado. Sus cuatro casos (keyNotFound, typeMismatch, valueNotFound y dataCorrupted) indican qué clave falló, en qué codingPath, qué se esperaba y qué llegó.

Cuando falle la decodificación, acostúmbrate a inspeccionar print(error) en lugar de print(error as? DecodingError); como mínimo, durante el desarrollo imprime codingPath por caso en el bloque catch. Esto reduce mucho el tiempo de depuración. En la mayoría de los “errores al analizar JSON”, la respuesta ya está dentro del error.

Resumen

  • La magia de Codable es la síntesis del compilador. Este escribe por ti el enum CodingKeys y init(from:)/encode(to:); personalizarlo consiste en decidir cuánto reemplazar manualmente.
  • Claves: con snake_case coherente, una línea de keyDecodingStrategy; con nombres irregulares, declara CodingKeys manualmente. Omitir un caso excluye ese campo de la conversión.
  • Fechas: especifica dateDecodingStrategy según el formato del servidor y cuidado con el locale en_US_POSIX y con ISO 8601 con milisegundos.
  • Anidamiento: por defecto, refleja el envoltorio; aplana solo los modelos principales con init(from:) + nestedContainer.
  • Fallos parciales: declaración honesta de opcionales + patrón FailableItem (try? + compactMap), y visibilidad mediante logs de los fallos ocultos.
  • Depuración: la respuesta está en el codingPath de DecodingError.

En la próxima entrega veremos el principio de la sintaxis con arroba, como @State y @Published, y los property wrappers. Crearemos directamente la “sintaxis que envuelve propiedades almacenadas” anunciada en la entrega sobre propiedades.

Seguir leyendo