Codable evita buena parte del código repetitivo necesario para convertir JSON, listas de propiedades y otros formatos en tipos de Swift. Sin embargo, cuando la estructura recibida no coincide con el modelo, la experiencia cambia por completo. El error contiene casi siempre la información necesaria para localizar el problema, pero hasta ahora aparecía enterrada entre nombres de tipos, valores opcionales y representaciones internas de cada CodingKey.
Swift 6.3 mejora esta situación mediante SE-0489. La propuesta hace que DecodingError y EncodingError adopten CustomDebugStringConvertible, de modo que su representación de depuración muestre el tipo de fallo, la ruta hasta el valor problemático y el contexto de una forma mucho más directa.
No cambia cómo se codifican o decodifican los datos, ni hace que Codable sea más flexible. Es una mejora de diagnóstico: la misma operación falla por el mismo motivo, pero ahora resulta mucho más fácil entender dónde.
El problema no era la falta de información
DecodingError ya proporcionaba cuatro casos con significado concreto: keyNotFound, typeMismatch, valueNotFound y dataCorrupted. Cada uno incluye un Context con una ruta de codificación, una descripción pensada para depuración y, cuando existe, el error subyacente.
Podemos verlo con este ejemplo de un tablón de salidas de trenes:
struct StationBoard: Decodable {
let station: String
let departures: [Departure]
}
struct Departure: Decodable {
let destination: String
let platform: Int
let delayed: Bool
}
let data = Data(#"""
{
"station": "Atocha",
"departures": [
{
"destination": "Toledo",
"platform": 7,
"delayed": false
},
{
"destination": "Cuenca",
"platform": "4A",
"delayed": true
}
]
}
"""#.utf8)
do {
_ = try JSONDecoder().decode(StationBoard.self, from: data)
} catch {
print(error)
}
El modelo espera un Int en platform, pero la segunda salida contiene la cadena "4A". Antes de Swift 6.3, el resultado de print(error) se parecía a este texto:
typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath:
[CodingKeys(stringValue: "departures", intValue: nil),
_CodingKey(stringValue: "Index 1", intValue: 1),
CodingKeys(stringValue: "platform", intValue: nil)],
debugDescription: "Expected to decode Int but found a string instead.",
underlyingError: nil))
El fallo, el tipo esperado y la ruta estaban presentes, pero había que reconstruir mentalmente departures[1].platform a partir de un array de claves. Cuanto más niveles tenía el modelo, más difícil resultaba separar la señal del ruido.
Qué muestra ahora Swift 6.3
Con la nueva representación, el mismo print(error) produce una salida semejante a esta:
DecodingError.typeMismatch: expected value of type Int.
Path: departures[1].platform.
Debug description: Expected to decode Int but found a string instead.
El formato real se imprime en una sola línea; aquí te lo muestro dividido para facilitar su lectura. La primera parte identifica el caso de DecodingError y el tipo que se intentaba obtener. Path convierte la sucesión de CodingKey en una ruta familiar: las propiedades se separan con puntos y los índices se escriben entre corchetes. Finalmente se conserva la descripción aportada por el decodificador.
Cuando la ruta está vacía, Swift omite por completo Path:. Si existe un error subyacente, también lo añade al final. Esto resulta especialmente útil con un JSON mal formado, porque dataCorrupted puede incorporar el error de JSONDecoder con detalles como la línea, la columna o el carácter inesperado.
No es necesario cambiar las llamadas existentes. print(error), debugPrint(error), String(describing:) y String(reflecting:) pueden aprovechar la nueva conformidad. La mejora reside en la librería estándar y no en una sobrecarga específica de JSONDecoder, por lo que también beneficia a otros codificadores y decodificadores que lancen estos tipos de error.
Los cuatro fallos de decodificación
La nueva presentación permite reconocer con rapidez qué contrato se ha incumplido:
| Caso | Qué significa | Ejemplo habitual |
|---|---|---|
keyNotFound | Falta una clave obligatoria | No llega destination y la propiedad no es opcional |
typeMismatch | El valor existe, pero tiene otro tipo | Llega "4A" donde se esperaba un Int |
valueNotFound | Se encuentra null para un tipo que no lo admite | delayed vale null, pero el modelo declara Bool |
dataCorrupted | Los datos no pueden interpretarse o incumplen una validación personalizada | El JSON tiene una coma sobrante o una fecha no cumple el formato esperado |
La distinción entre keyNotFound y valueNotFound es importante. Una propiedad no opcional falla tanto si la clave no existe como si contiene null, pero son contratos distintos y generan casos diferentes. Convertir la propiedad en opcional puede aceptar ambos escenarios, aunque no debería hacerse solo para silenciar el error: la opcionalidad tiene que representar el contrato real de la API.
dataCorrupted tampoco se limita a documentos JSON inválidos. Una implementación personalizada de init(from:) puede lanzar DecodingError.dataCorruptedError(in:debugDescription:) cuando el contenedor es correcto pero el valor no pertenece al dominio aceptado. Por ejemplo, una hora como "28:15" sigue siendo una cadena válida dentro de un JSON válido, pero puede ser un dato erróneo para el modelo de la aplicación.
EncodingError también mejora
La propuesta no se limita a la lectura de datos. EncodingError obtiene la misma representación comprensible. Su caso principal, invalidValue, aparece cuando un codificador no puede representar un valor.
struct TransferProgress: Encodable {
let identifier: UUID
let megabytesPerSecond: Double
}
let progress = TransferProgress(
identifier: UUID(),
megabytesPerSecond: .infinity
)
do {
_ = try JSONEncoder().encode(progress)
} catch {
print(error)
}
JSON no admite por defecto los valores no finitos de coma flotante. Swift 6.3 puede describir el fallo indicando el valor, su tipo y la ruta megabytesPerSecond, en lugar de volcar directamente la estructura completa del enum.
Este diagnóstico no sustituye a una decisión sobre el formato. Si el servicio admite cadenas para estos casos, JSONEncoder.NonConformingFloatEncodingStrategy.convertToString permite definir cómo representar infinito positivo, infinito negativo y NaN. Si no las admite, el error está señalando correctamente un valor que debe validarse antes de codificarlo.
Un texto para personas, no un nuevo contrato
La elección de CustomDebugStringConvertible es deliberada: debugDescription ofrece una representación útil durante el desarrollo, pero no constituye una interfaz estable. SE-0489 evita fijar el texto exacto para poder seguir mejorándolo. Por tanto, no conviene extraer la ruta con una expresión regular, buscar fragmentos de la frase para tomar decisiones ni enviar el mensaje sin procesar al usuario.
El código que necesite reaccionar ante un error debe seguir examinando el enum y su contexto:
func codingPath(_ keys: [any CodingKey]) -> String {
keys.reduce(into: "") { path, key in
if let index = key.intValue {
path += "[\(index)]"
} else {
path += path.isEmpty
? key.stringValue
: ".\(key.stringValue)"
}
}
}
func diagnostic(for error: DecodingError) -> String {
switch error {
case let .keyNotFound(key, context):
let container = codingPath(context.codingPath)
return "Falta '\(key.stringValue)' en \(container)"
case let .typeMismatch(type, context):
return "Se esperaba \(type) en \(codingPath(context.codingPath))"
case let .valueNotFound(type, context):
return "No hay un valor \(type) en \(codingPath(context.codingPath))"
case let .dataCorrupted(context):
return "Datos no válidos en \(codingPath(context.codingPath)): "
+ context.debugDescription
@unknown default:
return String(reflecting: error)
}
}
Esta función no pretende reemplazar la nueva descripción durante el desarrollo. Resulta útil cuando una aplicación necesita clasificar fallos, producir registros con un esquema propio o mantener una salida uniforme en varias versiones del sistema. También permite separar el mensaje técnico de la respuesta que verá el usuario.
Conviene evitar incluir el documento completo recibido en los registros. La ruta y el tipo de error suelen ser suficientes para diagnosticar una incompatibilidad y reducen el riesgo de guardar nombres, credenciales u otros datos sensibles. En telemetría, además, puede ser preferible normalizar los índices de los arrays para no crear una etiqueta diferente por cada posición.
Las pruebas no deberían depender de la frase completa
La nueva conformidad cambia el resultado de convertir estos errores en texto. Aunque sea una mejora compatible en código fuente, puede romper pruebas de instantánea o comprobaciones que comparasen literalmente la salida anterior. Esa representación nunca estuvo garantizada y la nueva tampoco lo está.
Una prueba más resistente verifica el caso y los campos estructurados:
func testInvalidPlatformReportsItsLocation() throws {
XCTAssertThrowsError(
try JSONDecoder().decode(StationBoard.self, from: data)
) { error in
guard let decodingError = error as? DecodingError,
case let .typeMismatch(type, context) = decodingError else {
return XCTFail("Se esperaba un error de tipo")
}
XCTAssertTrue(type == Int.self)
XCTAssertEqual(context.codingPath.last?.stringValue, "platform")
XCTAssertEqual(context.codingPath.dropLast().last?.intValue, 1)
}
}
Así la prueba expresa el comportamiento relevante: falló la conversión a Int, en la propiedad platform del segundo elemento. Un ajuste futuro en la puntuación o en las palabras de debugDescription no debería romperla.
La limitación del despliegue hacia atrás (retrocompatibilidad)
La conformidad con CustomDebugStringConvertible vive en la librería estándar y no se puede hacer retrocompatible en plataformas Apple con ABI estable. Compilar la aplicación con Swift 6.3 no garantiza por sí solo la nueva salida cuando se ejecuta sobre una versión anterior del sistema operativo: el entorno de ejecución también debe incluirla.
Esto puede provocar que una misma versión de la aplicación genere el formato nuevo en dispositivos recientes y el antiguo en los demás. Para la depuración local suele ser una diferencia menor. Para registros enviados a un servidor, pruebas ejecutadas sobre varias versiones o herramientas de línea de comandos que necesiten un formato uniforme, es mejor conservar un formateador propio basado en los casos de DecodingError y EncodingError.
Qué es lo que no soluciona esta mejora
Un mensaje más claro no recupera automáticamente los elementos erróneos de un array, no aplica valores predeterminados cuando falta una clave y no convierte una cadena en un número. Esas decisiones pertenecen al modelo y al contrato de datos: propiedades opcionales, implementaciones personalizadas de init(from:), estrategias del decodificador o envoltorios específicos para valores con mayor tolerancia.
Tampoco muestra necesariamente el fragmento exacto del documento alrededor del fallo. DecodingError.Context conoce la ruta y la descripción, pero no conserva siempre los bytes originales necesarios para reconstruir ese contexto. La propia propuesta deja esa posibilidad como trabajo futuro, ya que requeriría ampliar la información que transportan los errores o modificar los decodificadores de Foundation.
Swift 6.3 no vuelve más flexible a Codable; hace visible lo que ya sabía. Poder leer departures[1].platform de un vistazo reduce una tarea habitual de depuración a localizar el dato, comprobar el contrato y corregir el lado adecuado. Es un cambio pequeño en la API, pero elimina una fricción que acompañaba a Codable desde su llegada a Swift.