La implementación sintetizada de Codable se basa exclusivamente en la información de tipo estático disponible en tiempo de compilación. Al codificar una colección heterogénea de instancias de clase a través de una referencia de clase base, el compilador genera código encode(to:) que solo serializa las propiedades visibles para el tipo de clase base. Como resultado, las propiedades específicas de la subclase se omiten de la salida JSON, y durante la decodificación, el tiempo de ejecución carece de los metadatos necesarios para instanciar la subclase correcta, cayendo nuevamente en la clase base y perdiendo datos específicos del tipo.
Estábamos construyendo un panel de análisis financiero que procesaba varios tipos de transacciones para la gestión de carteras. El modelo de dominio utilizaba una jerarquía de clases donde Transaction era la clase base, con subclases como StockTrade, DividendPayment y FeeCharge que añadían propiedades específicas como tickerSymbol o dividendRate. La API de backend devolvía un array JSON mixto de estas transacciones, cada una conteniendo un campo discriminador transactionType.
Inicialmente, dependimos de la síntesis automática de Codable de Swift, asumiendo que manejaría el array polimórfico [Transaction]. Sin embargo, durante las pruebas de integración, descubrimos que la codificación de un array [StockTrade] convertido a [Transaction] resultaba en un JSON que solo contenía campos de la clase base como id y amount, omitiendo completamente tickerSymbol. Por el contrario, al decodificar este JSON se recreaban solo instancias base de Transaction, causando que la aplicación se bloqueara al intentar acceder a propiedades específicas de la subclase que se esperaba que existieran.
Consideramos tres enfoques distintos para resolver esta limitación. El primero involucraba la implementación manual de Codable donde añadíamos explícitamente el campo transactionType al contenedor de codificación e implementábamos un init(from:) personalizado que cambiaba según este discriminador para instanciar la subclase correcta. Este enfoque ofrecía una completa seguridad de tipos y preservaba el gráfico de objetos existente, pero requería escribir y mantener un código boilerplate significativo para cada nuevo tipo de transacción, aumentando el riesgo de errores del desarrollador al añadir características.
La segunda solución exploró el uso de un envoltorio AnyCodable que elimina tipos o un enfoque orientado a protocolos con tipos existenciales (any TransactionProtocol). Si bien esto permitía almacenar tipos heterogéneos en un array sin herencia, sacrificaba la seguridad de tipos en tiempo de compilación e introducía sobrecarga en tiempo de ejecución debido a la envoltura existencial y el despacho dinámico. También complicaba el contrato de API al obligar a los consumidores a manejar los artefactos de eliminación de tipos y el casting, reduciendo la claridad del código.
La tercera opción fue refactorizar la jerarquía de clases en un solo enum con valores asociados, como enum Transaction { case stock(StockData), case dividend(DividendData) }. Los enums soportan naturalmente la serialización polimórfica a través de Codable sintetizado, ya que el compilador genera automáticamente un campo discriminador. Sin embargo, esto habría requerido una refactorización masiva del modelo Core Data existente y la lógica empresarial a lo largo de la aplicación, llevando un riesgo de regresión inaceptable para un sistema en producción.
Seleccionamos la primera solución: implementación manual de Codable con un campo discriminador, porque localizó cambios en la capa de serialización sin interrumpir la arquitectura existente o el esquema de base de datos. Implementamos un método de fábrica en la clase base que decodificaba primero el identificador de tipo, y luego delegaba al inicializador de la subclase apropiada basado en el valor de cadena.
El resultado fue una robusta canalización de serialización que manejó correctamente las respuestas API polimórficas con plena fidelidad de tipos. Aunque requería aproximadamente 200 líneas de código de parsing manual, mantenía la compatibilidad con las funciones existentes y proporcionaba errores claros en tiempo de compilación cuando los desarrolladores añadían nuevos tipos de transacción pero olvidaban actualizar la lógica de decodificación, previniendo fallos en tiempo de ejecución.
¿Por qué el casting de un [Subclass] a un [BaseClass] antes de codificar con JSONEncoder causa pérdida de datos para propiedades específicas de la subclase?
El método sintetizado encode(to:) se despacha estáticamente en función del tipo de tiempo de compilación del valor en la colección. Al convertir a [BaseClass], el compilador selecciona la implementación sintetizada de BaseClass, que solo itera sobre las propiedades declaradas en BaseClass. Las propiedades de la subclase son invisibles para esta implementación porque el mecanismo de despacho estático no consulta los metadatos del tipo dinámico para los métodos sintetizados. Para preservar todas las propiedades, debes codificar utilizando el tipo concreto o implementar manualmente la resolución de tipos dinámicos a través de un campo discriminador.
¿Cómo interactúa el requisito de un inicializador requerido con la conformidad Decodable en jerarquías de clases, y por qué esto previene la instanciación automática de subclases?
Decodable requiere un inicializador init(from: Decoder). Para las clases, esto debe estar marcado como required en la clase base para permitir que las subclases hereden la conformidad. Sin embargo, la implementación sintetizada en la clase base no puede determinar dinámicamente qué subclase instanciar basada en datos externos como un campo discriminador. Cuando el decodificador se encuentra con datos que representan una subclase, llama al init(from:) de la clase base, que solo sabe cómo inicializar la porción de clase base. Para soportar la decodificación polimórfica, los desarrolladores deben sobrescribir init(from:) en cada subclase e implementar un método de fábrica que inspeccione el contenedor del decodificador para determinar el tipo concreto antes de la instanciación.
¿Cuál es la diferencia fundamental entre cómo Swift sintetizado Codable maneja enums con valores asociados versus herencia de clases, y por qué esto hace que los enums sean adecuados para la serialización polimórfica?
Swift genera una clave discriminadora al sintetizar Codable para enums con valores asociados. La codificación incluye el nombre del caso como una clave de cadena, y la implementación de decodificación cambia según esta clave para reconstruir el caso correcto y su carga asociada. Esto funciona porque los enums forman una jerarquía de tipos cerrada y sellada conocida exhaustivamente en tiempo de compilación, lo que permite al compilador generar una declaración switch completa. En contraste, las clases forman una jerarquía abierta donde se pueden agregar nuevas subclases en diferentes módulos. El compilador no puede generar un switch exhaustivo para todas las posibles subclases al sintetizar la conformidad Codable de la clase base, haciéndolo imposible manejar automáticamente el polimorfismo sin intervención manual.