Para manter um modelo Swift Codable compatível com representações antigas em memória, torne opcionais os valores realmente ausentes e implemente explicitamente alternativas de decodificação. Adicionar apenas um valor padrão à declaração não faz com que um campo codificado ausente seja decodificado com sucesso.
Este guia destina-se a um desenvolvedor para Mac que está atualizando um modelo simples de layout para desktop. A tarefa concreta é permitir que o modelo revisado aceite representações sintéticas antigas, preservando os valores fornecidos e informando tipos incompatíveis. Todos os exemplos criam e consomem valores em memória.
O que muda quando um campo se torna opcional ou recebe um valor padrão?
Codable combina codificação e decodificação. O Swift pode sintetizar essas implementações quando as propriedades armazenadas dão suporte aos protocolos necessários. Isso elimina código rotineiro, mas uma alteração que ainda compila pode mudar quais representações o modelo aceita. A Apple documenta tanto a conformidade automática quanto implementações personalizadas em Encoding and Decoding Custom Types.
Considere um bloco de layout com título e subtítulo obrigatórios. Agora você quer que o subtítulo seja opcional e que uma nova quantidade de colunas tenha o valor padrão três. São duas decisões distintas: o subtítulo pode não ter valor, enquanto a quantidade de colunas precisa sempre ter um valor utilizável após a decodificação.
Para propriedades mutáveis comuns incluídas na decodificação sintetizada, o Swift trata uma propriedade opcional como permitindo uma chave ausente ou um valor nulo. Um inicializador na declaração não é uma alternativa de decodificação: uma propriedade opcional inicializada com um valor não nulo ainda pode ser decodificada como nil, e uma propriedade não opcional com um valor padrão na declaração ainda pode falhar quando sua chave está ausente. A explicação da síntese de Codable da equipe do Swift descreve esse comportamento intencional.
A tabela de decisão a seguir traduz essas regras em uma política para campos. Ela pressupõe um campo inteiro e um valor presente que possa ser convertido para Int. As duas últimas linhas exigem decodificação personalizada.
| Política do campo | Chave ausente | Nulo explícito | Inteiro válido | Tipo incompatível |
|---|---|---|---|---|
var columns: Int sintetizado |
Erro | Erro | Usar valor | Erro |
var columns: Int = 3 sintetizado |
Erro | Erro | Usar valor | Erro |
var columns: Int? sintetizado |
nil |
nil |
Usar valor | Erro |
| Alternativa personalizada para ausência e nulo | 3 |
3 |
Usar valor | Erro |
| Alternativa personalizada apenas para ausência | 3 |
Erro | Usar valor | Erro |
A distinção decorre dos métodos do contêiner. decode(_:forKey:) exige uma entrada e um valor não nulo que possa ser convertido. decodeIfPresent(_:forKey:) retorna nil tanto para ausência quanto para nulo, mas ainda lança um erro para um tipo incompatível. Aplicar ?? 3 a esse resultado fornece a alternativa.
Implemente e verifique a política de compatibilidade em memória
Use este exemplo em um contexto de execução do Swift no seu Mac, com Foundation disponível e suporte a instruções de nível superior, como um playground existente. Execute o bloco inteiro de uma vez. JSONEncoder do Foundation produz uma representação Data, e JSONDecoder cria um novo valor tipado a partir dela. O exemplo não usa nenhum documento codificado literal nem entrada externa.
A política escolhida é explícita: title continua obrigatório; um subtitle ausente ou nulo se torna nil; um columns ausente ou nulo se torna três. Um valor presente de coluna com tipo incompatível continua sendo um erro. O inicializador personalizado trata da decodificação, enquanto a codificação continua sintetizada com as CodingKeys do modelo. A Apple documenta essa abordagem de personalização em Encoding and Decoding Custom Types.
Este é um programa completo, incluindo dados de teste sintéticos e verificações. O Swift não estava disponível no ambiente de pesquisa; por isso, o exemplo foi conferido com base nas APIs documentadas, mas não foi executado.
import Foundation
struct LegacyTile: Codable {
var title: String
var subtitle: String
}
struct Tile: Codable, Equatable {
static let defaultColumns = 3
var title: String
var subtitle: String?
var columns: Int = Tile.defaultColumns
enum CodingKeys: String, CodingKey {
case title, subtitle, columns
}
}
extension Tile {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
title = try values.decode(String.self, forKey: .title)
subtitle = try values.decodeIfPresent(String.self, forKey: .subtitle)
columns = try values.decodeIfPresent(Int.self, forKey: .columns)
?? Self.defaultColumns
}
}
struct MinimalTile: Encodable {
var title: String
}
struct NullTile: Encodable {
enum CodingKeys: String, CodingKey {
case title, subtitle, columns
}
func encode(to encoder: Encoder) throws {
var values = encoder.container(keyedBy: CodingKeys.self)
try values.encode("Circle", forKey: .title)
try values.encodeNil(forKey: .subtitle)
try values.encodeNil(forKey: .columns)
}
}
struct WrongColumnsTile: Encodable {
var title: String
var columns: String
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// An older representation keeps its subtitle and gains the fallback.
let legacyBytes = try encoder.encode(
LegacyTile(title: "Circle", subtitle: "Blue")
)
let upgraded = try decoder.decode(Tile.self, from: legacyBytes)
precondition(upgraded.title == "Circle")
precondition(upgraded.subtitle == "Blue")
precondition(upgraded.columns == 3)
// Missing keys and explicit nulls follow the chosen policy.
let minimalBytes = try encoder.encode(MinimalTile(title: "Circle"))
let minimal = try decoder.decode(Tile.self, from: minimalBytes)
precondition(minimal.subtitle == nil)
precondition(minimal.columns == 3)
let nullBytes = try encoder.encode(NullTile())
let explicitNull = try decoder.decode(Tile.self, from: nullBytes)
precondition(explicitNull == minimal)
// A supplied nondefault value survives an encode/decode cycle.
let supplied = Tile(title: "Square", subtitle: "Green", columns: 5)
let suppliedBytes = try encoder.encode(supplied)
let restored = try decoder.decode(Tile.self, from: suppliedBytes)
precondition(restored == supplied)
// Incompatible types must remain visible as decoding failures.
let wrongBytes = try encoder.encode(
WrongColumnsTile(title: "Circle", columns: "wide")
)
do {
_ = try decoder.decode(Tile.self, from: wrongBytes)
preconditionFailure("Expected a type mismatch")
} catch DecodingError.typeMismatch(_, let context) {
precondition(context.codingPath.last?.stringValue == "columns")
}
// A newer representation may no longer satisfy an older reader.
let currentBytes = try encoder.encode(minimal)
do {
_ = try decoder.decode(LegacyTile.self, from: currentBytes)
preconditionFailure("Expected the required subtitle to be missing")
} catch DecodingError.keyNotFound(let key, _) {
precondition(key.stringValue == "subtitle")
}
print("All in-memory compatibility checks passed.")
A saída final esperada é All in-memory compatibility checks passed. Erros inesperados são propagados, em vez de convertidos em sucesso. Os dados de teste cobrem deliberadamente representações diferentes: MinimalTile omite campos, enquanto NullTile usa encodeNil(forKey:) para criar entradas explicitamente nulas.
Manter init(from:) em uma extensão também deixa disponível o inicializador membro a membro da estrutura, permitindo que o exemplo construa Tile(title:subtitle:columns:). O inicializador personalizado de decodificação atribui valores a todas as propriedades de instância armazenadas, então o resultado fica totalmente inicializado antes de ser retornado.
O nulo deve usar o valor padrão ou continuar sendo um erro?
Escolha de acordo com o significado do campo. Para este layout ilustrativo, tratar tanto a ausência quanto o nulo como a quantidade padrão de colunas é uma política razoável. Ainda assim, essa é uma decisão da aplicação: decodeIfPresent combina deliberadamente esses dois estados, e a alternativa lhes dá o mesmo resultado. O comportamento de retorno documentado deixa essa consequência explícita.
Se a ausência significar “usar o layout padrão”, mas um nulo fornecido dever gerar erro, substitua apenas a atribuição de columns dentro de init(from:) do programa por este trecho. Todos os demais pré-requisitos e definições permanecem iguais.
// Replacement fragment inside Tile.init(from:).
if values.contains(.columns) {
columns = try values.decode(Int.self, forKey: .columns)
} else {
columns = Self.defaultColumns
}
contains(_:) informa se existe uma entrada, inclusive uma entrada nula. Portanto, um nulo explícito segue o ramo decode e produz valueNotFound; somente a ausência recebe o valor padrão. Com essa política revisada, a verificação do dado de teste nulo no programa original deve passar a esperar esse erro, em vez de igualdade com minimal.
Evite transformar a atribuição em (try? values.decode(Int.self, forKey: .columns)) ?? 3, a menos que a intenção seja descartar todas as falhas de decodificação. Essa expressão também substitui por três um valor fornecido incompatível. Usar try decodeIfPresent preserva a distinção entre uma omissão aceita e uma representação que o campo não consegue interpretar, conforme especificado pelo contrato de erros do método de decodificação.
A compatibilidade funciona nos dois sentidos?
Verifique cada leitor separadamente. O programa demonstra que o leitor revisado aceita a representação antiga. Também demonstra um limite: o leitor antigo ainda exige subtitle, então rejeita uma representação mais nova em que esse campo foi omitido. Tornar opcional a nova propriedade do Swift não altera os requisitos do tipo antigo.
A codificação sintetizada de valores opcionais omite os valores nil por meio de codificação condicional. O encodeIfPresent(_:forKey:) do Swift documenta que ele codifica apenas valores não nulos. Também é possível codificar um nulo explícito com encodeNil, mas isso ainda não satisfaz uma propriedade String não opcional antiga que exige um valor.
Quando um leitor antigo ainda precisa funcionar, defina uma representação explícita que atenda aos requisitos dele. Neste exemplo sintético, um adaptador poderia construir um LegacyTile com um subtítulo escolhido antes da codificação. Decidir se um subtítulo ausente deve se tornar uma string específica é uma decisão separada do produto; não invente esse significado silenciosamente dentro de uma alternativa geral.
Diferencie também a preservação do valor da preservação da representação. No exemplo, um subtítulo omitido e um nulo explícito se tornam o mesmo valor do Swift, nil. Portanto, o modelo não consegue reproduzir qual dessas duas entradas recebeu. Da mesma forma, decodificar a quantidade de colunas ausente como três e recodificar o modelo fornece uma quantidade concreta de colunas. Essas são consequências diretas do modelo e da política de decodificação escolhidos.
O que você deve verificar antes de alterar o modelo?
Use uma pequena matriz de compatibilidade sempre que um campo mudar. Comece com uma representação antiga e, em seguida, varie um campo de cada vez. Assim, uma ida e volta bem-sucedida não oculta uma falha que ocorre apenas quando uma chave está ausente.
- Entrada antiga: confirme que o leitor revisado a aceita e preserva os valores existentes.
- Campo ausente: confirme que o resultado é o valor opcional, a alternativa ou o erro pretendido.
- Nulo explícito: teste-o separadamente da omissão.
- Valor fornecido: use um valor diferente do padrão para comprovar que a decodificação não o sobrescreve.
- Tipo incompatível: confirme que ainda lança um erro quando não há uma política de conversão.
- Leitor antigo: verifique se a representação codificada revisada ainda atende aos requisitos dele.
Não remova um campo de CodingKeys apenas para obter uma alternativa. A Apple explica que propriedades omitidas são excluídas da representação codificada e decodificada e precisam de valores padrão para a conformidade de decodificação automática. Isso é adequado quando uma propriedade deve ficar fora da representação; também significa que um valor fornecido para essa propriedade não será lido. Consulte as orientações da Apple sobre chaves de codificação.
Mantenha a regra de compatibilidade junto à lógica de decodificação do campo: opcional para uma ausência significativa, uma alternativa explícita para uma omissão aceita e decodificação normal com lançamento de erro para valores obrigatórios. Os dados de teste em memória tornam o comportamento pretendido verificável sempre que o modelo muda.
Fontes
- Encoding and Decoding Custom Types — Apple
- JSONEncoder — Apple
- JSONDecoder — Apple
- Decoding of optionals missing in json — Swift Forums
- decodeIfPresent(_:forKey:) — Apple
- decode(_:forKey:) — Apple
- contains(_:) — Apple
- encodeNil(forKey:) — Apple
- encodeIfPresent(_:forKey:) — Apple