Codable avanzado en Swift: CodingKeys propias, estrategias de fecha, polimorfismo y anidamiento

Codable (la combinación de Encodable y Decodable) es el sistema estándar de Swift para serializar y deserializar datos, principalmente JSON. La síntesis automática funciona bien cuando la estructura Swift mapea perfectamente al JSON. En cuanto los nombres difieren, hay fechas, tipos polimórficos o estructuras anidadas profundas, necesitas las técnicas avanzadas que Codable ofrece.

CodingKeys propias: renombrar campos

Cuando los nombres del JSON no coinciden con tus propiedades Swift (por ejemplo, la API devuelve user_name y tú quieres userName), defines un enum CodingKeys:

struct Usuario: Codable {
    let id: Int
    let userName: String
    let emailAddress: String
    let createdAt: Date

    enum CodingKeys: String, CodingKey {
        case id
        case userName = "user_name"
        case emailAddress = "email_address"
        case createdAt = "created_at"
    }
}

// JSON que parsea correctamente:
// { "id": 1, "user_name": "ana", "email_address": "[email protected]", "created_at": "..." }

DateDecodingStrategy: formatos de fecha

Las fechas son el punto de fricción más habitual. JSONDecoder tiene varias estrategias incorporadas y permite personalización total:

let decoder = JSONDecoder()

// ISO 8601: "2024-03-15T10:30:00Z"
decoder.dateDecodingStrategy = .iso8601

// Timestamp Unix (segundos desde 1970): 1710497400
decoder.dateDecodingStrategy = .secondsSince1970

// Formato personalizado
let formateador = DateFormatter()
formateador.dateFormat = "dd/MM/yyyy HH:mm"
formateador.locale = Locale(identifier: "es_ES")
decoder.dateDecodingStrategy = .formatted(formateador)

// Totalmente personalizado (para múltiples formatos en el mismo JSON)
decoder.dateDecodingStrategy = .custom { decoder in
    let contenedor = try decoder.singleValueContainer()
    let cadena = try contenedor.decode(String.self)

    if let fecha = ISO8601DateFormatter().date(from: cadena) {
        return fecha
    }
    let df = DateFormatter()
    df.dateFormat = "yyyy-MM-dd"
    if let fecha = df.date(from: cadena) { return fecha }

    throw DecodingError.dataCorruptedError(
        in: contenedor,
        debugDescription: "Formato de fecha no reconocido: (cadena)"
    )
}

Decodificación anidada con nestedContainer

A veces el JSON tiene una estructura más profunda que tu modelo Swift. nestedContainer permite acceder a claves anidadas sin crear tipos intermedios:

// JSON: { "id": 1, "address": { "city": "Madrid", "country": "ES" } }
struct Usuario: Decodable {
    let id: Int
    let ciudad: String
    let pais: String

    enum CodingKeys: String, CodingKey {
        case id, address
    }
    enum AddressKeys: String, CodingKey {
        case city, country
    }

    init(from decoder: Decoder) throws {
        let contenedor = try decoder.container(keyedBy: CodingKeys.self)
        id = try contenedor.decode(Int.self, forKey: .id)

        let address = try contenedor.nestedContainer(keyedBy: AddressKeys.self, forKey: .address)
        ciudad = try address.decode(String.self, forKey: .city)
        pais = try address.decode(String.self, forKey: .country)
    }
}

Tipos polimórficos con campo discriminador

Un patrón muy frecuente en APIs: un array que puede contener objetos de tipos distintos identificados por un campo type:

// JSON:
// [
//   {"type": "texto", "contenido": "Hola mundo"},
//   {"type": "imagen", "url": "https://...", "alt": "Foto"},
//   {"type": "video", "url": "https://...", "duracion": 120}
// ]

enum Bloque: Decodable {
    case texto(ContenidoTexto)
    case imagen(ContenidoImagen)
    case video(ContenidoVideo)

    enum TipoKey: String, CodingKey { case type }

    init(from decoder: Decoder) throws {
        let tipoContainer = try decoder.container(keyedBy: TipoKey.self)
        let tipo = try tipoContainer.decode(String.self, forKey: .type)

        switch tipo {
        case "texto":
            self = .texto(try ContenidoTexto(from: decoder))
        case "imagen":
            self = .imagen(try ContenidoImagen(from: decoder))
        case "video":
            self = .video(try ContenidoVideo(from: decoder))
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type,
                in: tipoContainer,
                debugDescription: "Tipo desconocido: (tipo)"
            )
        }
    }
}

struct ContenidoTexto: Decodable { let contenido: String }
struct ContenidoImagen: Decodable { let url: URL; let alt: String }
struct ContenidoVideo: Decodable { let url: URL; let duracion: Int }

Property wrappers para valores por defecto

Cuando el JSON puede omitir campos opcionales que quieres representar como no opcionales en Swift:

@propertyWrapper
struct DefaultValue<T: Decodable>: Decodable {
    var wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        wrappedValue = (try? container.decode(T.self)) ?? wrappedValue
    }
}

// Protocolo para valores por defecto tipados
protocol DefaultValueProvider { static var defaultValue: Self { get } }
extension Bool: DefaultValueProvider { static var defaultValue = false }
extension Int: DefaultValueProvider { static var defaultValue = 0 }
extension String: DefaultValueProvider { static var defaultValue = "" }

// Macro alternativa disponible con Swift 5.9+:
// @Default(false) var activo: Bool

struct Configuracion: Decodable {
    var nombre: String
    var activo: Bool = false  // Funciona si el campo puede faltar (Codable custom)
    var version: Int = 1
}

Encodable personalizado: control sobre la salida

struct Pedido: Encodable {
    let id: Int
    let items: [ItemPedido]
    let fecha: Date
    let estado: EstadoPedido

    func encode(to encoder: Encoder) throws {
        var contenedor = encoder.container(keyedBy: CodingKeys.self)
        try contenedor.encode(id, forKey: .id)
        try contenedor.encode(items, forKey: .items)

        // Fecha en formato personalizado
        let df = ISO8601DateFormatter()
        try contenedor.encode(df.string(from: fecha), forKey: .fecha)

        // Solo incluir estado si no está pendiente
        if estado != .pendiente {
            try contenedor.encode(estado.rawValue, forKey: .estado)
        }
    }
}

KeyDecodingStrategy: snake_case automático

Si toda tu API usa snake_case, en lugar de definir CodingKeys para cada tipo puedes usar la estrategia del decoder:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
// "user_name" ? userName, "created_at" ? createdAt

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
// userName ? "user_name", createdAt ? "created_at"

Resumen

Codable avanzado gira en torno a cuatro herramientas: CodingKeys para renombrar campos, las estrategias del decoder/encoder para fechas y naming conventions, nestedContainer para JSON con estructura más profunda que el modelo, y la implementación manual de init(from:) para tipos polimórficos. Con estas técnicas puedes parsear prácticamente cualquier JSON real sin necesidad de librerías externas.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP