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.
