L'implementazione Codable sintetizzata si basa esclusivamente su informazioni di tipo statico disponibili al momento della compilazione. Quando si codifica una collezione eterogenea di istanze di classi tramite un riferimento di classe base, il compilatore genera codice encode(to:) che serializza solo le proprietà visibili al tipo di classe base. Di conseguenza, le proprietà specifiche delle sottoclassi vengono omesse dall'output JSON, e durante la decodifica, il runtime non possiede i metadati necessari per istanziare la corretta sottoclasse, tornando alla classe base e perdendo dati specifici del tipo.
Stavamo costruendo un cruscotto di analisi finanziaria che elaborava vari tipi di transazioni per la gestione del portafoglio. Il modello del dominio utilizzava una gerarchia di classi in cui Transaction era la classe base, con sottoclassi come StockTrade, DividendPayment e FeeCharge che aggiungevano proprietà specifiche come tickerSymbol o dividendRate. L'API backend restituiva un array JSON misto di queste transazioni, ognuna contenente un campo discriminante transactionType.
Inizialmente ci siamo affidati alla sintesi automatica di Codable di Swift, assumendo che avrebbe gestito l'array polimorfico [Transaction]. Tuttavia, durante i test di integrazione, abbiamo scoperto che la codifica di un array [StockTrade] convertito in [Transaction] risultava in un JSON che conteneva solo campi della classe base come id e amount, omettendo completamente tickerSymbol. Al contrario, la decodifica di questo JSON ricreava solo istanze di base Transaction, causando il crash dell'app quando tentava di accedere a proprietà specifiche delle sottoclassi che ci si aspettava fossero presenti.
Abbiamo considerato tre approcci distinti per risolvere questa limitazione. Il primo prevedeva un'implementazione manuale di Codable in cui aggiungevamo esplicitamente il campo transactionType al contenitore di codifica e implementavamo un costruttore personalizzato init(from:) che passava a questo discriminante per istanziare la corretta sottoclasse. Questo approccio offriva sicurezza di tipo completa e preservava il grafo oggetto esistente, ma richiedeva di scrivere e mantenere un significativo codice boilerplate per ogni nuovo tipo di transazione, aumentando il rischio di errore per gli sviluppatori quando si aggiungevano funzionalità.
La seconda soluzione prevedeva l'uso di un wrapper AnyCodable a tipo eroso o un approccio orientato ai protocolli con tipi esistenziali (any TransactionProtocol). Sebbene ciò consentisse di memorizzare tipi eterogenei in un array senza eredità, sacrificava la sicurezza di tipo al momento della compilazione e introduceva un sovraccarico di runtime a causa del boxing esistenziale e del dispatch dinamico. Complicava anche il contratto API costringendo i consumatori a gestire artefatti di cancellazione del tipo e casting, riducendo la chiarezza del codice.
La terza opzione era rifattorizzare la gerarchia di classi in un singolo enum con valori associati, come enum Transaction { case stock(StockData), case dividend(DividendData) }. Gli enum supportano naturalmente la serializzazione polimorfica tramite Codable sintetizzato, poiché il compilatore genera automaticamente un campo discriminante. Tuttavia, questo avrebbe richiesto un'enorme rifattorizzazione del modello Core Data esistente e della logica aziendale in tutta l'applicazione, portando a un rischio di regressione inaccettabile per un sistema di produzione.
Abbiamo scelto la prima soluzione: implementazione manuale di Codable con un campo discriminante, poiché localizzava le modifiche al layer di serializzazione senza interrompere l'architettura esistente o lo schema del database. Abbiamo implementato un metodo factory nella classe base che decodificava prima l'identificatore di tipo, poi delegava al costruttore appropriato della sottoclasse in base al valore della stringa.
Il risultato è stato una robusta pipeline di serializzazione che gestiva correttamente le risposte API polimorfiche con piena fedeltà di tipo. Sebbene richiedesse circa 200 righe di codice di parsing manuale, mantenne la compatibilità retroattiva con le funzionalità esistenti e fornì errori chiari al momento della compilazione quando gli sviluppatori aggiungevano nuovi tipi di transazione ma dimenticavano di aggiornare la logica di decodifica, prevenendo fallimenti a runtime.
Perché il casting di un [Subclass] in [BaseClass] prima della codifica con JSONEncoder causa la perdita di dati per le proprietà specifiche delle sottoclassi?
Il metodo sintetizzato encode(to:) viene inviato in modo statico in base al tipo di compilazione del valore nella collezione. Quando ti converti in [BaseClass], il compilatore seleziona l'implementazione sintetizzata di BaseClass, che itera solo sulle proprietà dichiarate in BaseClass. Le proprietà delle sottoclassi sono invisibili a questa implementazione poiché il meccanismo di dispatch statico non consulta i metadati del tipo dinamico per i metodi sintetizzati. Per preservare tutte le proprietà, devi codificare utilizzando il tipo concreto o implementare manualmente la risoluzione del tipo dinamico attraverso un campo discriminante.
Come interagisce il requisito per un inizializzatore richiesto con la conformità a Decodable nelle gerarchie di classi, e perché questo impedisce l'istanziazione automatica della sottoclasse?
Decodable richiede un inizializzatore init(from: Decoder). Per le classi, questo deve essere contrassegnato come required nella classe base per consentire alle sottoclassi di ereditare la conformità. Tuttavia, l'implementazione sintetizzata nella classe base non può determinare dinamicamente quale sottoclasse istanziare in base a dati esterni come un campo discriminante. Quando il decoder incontra dati che rappresentano una sottoclasse, chiama il init(from:) della classe base, che sa solo come inizializzare la porzione della classe base. Per supportare la decodifica polimorfica, gli sviluppatori devono sovrascrivere init(from:) in ogni sottoclasse e implementare un metodo factory che ispeziona il contenitore del decoder per determinare il tipo concreto prima dell'istanziazione.
Qual è la differenza fondamentale tra il modo in cui Swift gestisce le enum con valori associati sintetizzati rispetto all'ereditarietà delle classi, e perché questo rende le enum adatte per la serializzazione polimorfica?
Swift genera una chiave discriminante quando sintetizza Codable per enum con valori associati. La codifica include il nome del caso come chiave stringa, e l'implementazione di decodifica si basa su questa chiave per ricostruire il caso corretto e il suo payload associato. Questo funziona perché gli enum formano una gerarchia di tipo chiusa e sigillata nota in modo esaustivo al momento della compilazione, consentendo al compilatore di generare un'istruzione switch completa. Al contrario, le classi formano una gerarchia aperta in cui è possibile aggiungere nuove sottoclassi in diversi moduli. Il compilatore non può generare uno switch esaustivo per tutte le possibili sottoclassi quando sintetizza la conformità Codable della classe base, rendendo impossibile gestire automaticamente il polimorfismo senza intervento manuale.