Swift e Objective-C

[Swift intermediário #6] Codable avançado em Swift: guia para quatro armadilhas

Do que a síntese automática de Codable realmente gera às soluções padrão para chaves incompatíveis, formatos de data, estruturas aninhadas e falhas parciais, incluindo a depuração de DecodingError.

6 min de leitura
Imagem de capa de [Swift intermediário #6] Codable avançado em Swift: guia para quatro armadilhas

Codable parece mágica à primeira vista. Você adiciona : Codable cinco letras a uma struct e a conversão para JSON surge de graça. Mas a mágica desaparece ao conectar uma API real: o servidor usa snake_case, enquanto Swift usa camelCase; os formatos de data variam por API; e um único item inválido pode fazer toda a decodificação falhar, deixando a tela vazia.

A sexta parte da série intermediária aprofunda Codable: o que a síntese realmente gera e as soluções padrão para quatro situações inevitáveis em produção: chaves, datas, aninhamento e falhas parciais.

A verdade por trás da mágica — código escrito pelo compilador

Codable é o typealias de Encodable & Decodable e dos protocolos que exigem saber como escrever a si próprio em um encoder e como criar a si próprio a partir de um decoder. O que parece mágica vem da síntese automática do compilador. Se todas as propriedades armazenadas forem Codable, o compilador gera duas coisas para você.

Primeiro, um enum CodingKeys: uma lista de chaves com cases que repetem os nomes das propriedades. Segundo, as implementações de init(from:) e encode(to:), que leem e escrevem cada propriedade usando essas chaves. Personalizar Codable é decidir qual desses dois resultados substituir manualmente. Se apenas as chaves diferem, use CodingKeys; se a estrutura também muda, implemente init(from:). Com esse modelo mental, todo o restante vira uma questão de quanto código escrever à mão.

Outro fato importante: Codable não é exclusivo de JSON. Como encoders e decoders podem ser substituídos, você pode usar PropertyListEncoder para plist ou bibliotecas de terceiros para XML e YAML. O tipo sabe apenas como se representar; o encoder decide o formato. Isso é separação de responsabilidades.

Situação 1. Nomes de chave diferentes — CodingKeys e estratégias de chave

Se o servidor envia user_name, mas você quer usar userName como propriedade, há duas etapas para a solução.

Se a regra for consistente globalmente, uma linha na configuração do decoder resolve: decoder.keyDecodingStrategy = .convertFromSnakeCase. Todas as chaves são convertidas automaticamente de snake_case para camelCase. Se toda a API segue essa convenção, essa é a resposta certa.

Se as regras forem inconsistentes ou você quiser renomear a chave, escreva CodingKeys manualmente.

struct User: Codable {
    let userName: String
    let signupDate: Date

    enum CodingKeys: String, CodingKey {
        case userName = "user_nm"     // chave legada do servidor
        case signupDate = "created"
    }
}

Há um efeito colateral importante: remover um case de CodingKeys exclui a propriedade da codificação e da decodificação. Isso é útil para estados locais, como flags de cache, mas propriedades excluídas precisam ter valores padrão.

Diagrama de pipeline JSON mostrando quatro áreas: snake case, datas, aninhamento e falhas parciais
As quatro grandes armadilhas de Codable em produção: chaves, datas, aninhamento e falhas parciais

Situação 2. Datas — um campo minado de formatos

Date é a armadilha mais comum de Codable em produção. JSON não define um padrão único de data, então os servidores usam timestamps Unix (1720000000), ISO 8601 (“2026-07-15T09:30:00Z”) ou formatos personalizados (“2026-07-15 09:30”).

A solução é configurar dateDecodingStrategy de acordo com o formato do servidor: .secondsSince1970, .iso8601 ou .formatted(formatter) para um formato personalizado. Usar um DateFormatter personalizado tem duas armadilhas. Se o locale não for fixado em en_US_POSIX, o parsing pode falhar conforme a configuração de 12/24 horas do usuário. Além disso, .iso8601 falha quando o servidor envia ISO 8601 com milissegundos; ative a opção de milissegundos no ISO8601DateFormatter. A maioria dos bugs misteriosos em que o parsing de data falha apenas para alguns usuários vem dessas duas armadilhas.

Se os campos da mesma API usarem formatos diferentes, o caminho prático é receber apenas esse campo como String e convertê-lo em uma propriedade computada, ou ramificar com a estratégia .custom.

Situação 3. Aninhamento e estruturas incompatíveis — o formato do servidor versus o meu

É quando o JSON do servidor chega profundamente encapsulado, como em {"data": {"user": {...}}}. A solução mais simples é criar o tipo de envelope exatamente assim: espelhar a estrutura do servidor com struct Envelope: Codable { let data: DataBox } e extrair o valor no ponto de chamada usando envelope.data.user. É explícito e fácil de depurar, por isso é o melhor padrão para produção.

Se quiser um modelo diferente do servidor, como um User plano, implemente init(from:) manualmente e atravesse a hierarquia com nestedContainer. O código aumenta, mas o modelo fica mais limpo e centrado no domínio. O critério é o alcance de uso: um modelo central usado no app inteiro justifica um init(from:) escrito à mão; uma resposta de uma tela fica mais econômica com o espelhamento do envelope.

Situação 4. Falha parcial — quando um item derruba tudo

É a situação mais dolorosa em produção. Se um campo obrigatório for null em uma lista de 100 produtos, a decodificação do array inteiro lança um erro e a tela fica vazia. Como vimos na parte sobre tratamento de erros, erros lançados são propagados.

A primeira linha de defesa são os optionals. Declare honestamente como let thumbnail: URL? os campos que o servidor pode omitir. O princípio de registrar no tipo que algo “pode não existir” aplica-se diretamente ao design do modelo.

A defesa estrutural é um wrapper tolerante a falhas. Um padrão comum é criar um wrapper genérico que transforme falhas de decodificação de elementos em nil.

struct FailableItem<T: Decodable>: Decodable {
    let value: T?
    init(from decoder: Decoder) throws {
        value = try? T(from: decoder)   // nil em caso de falha nil
    }
}

let items = try decoder.decode([FailableItem<Product>].self, from: data)
    .compactMap(\.value)   // manter apenas os que tiveram sucesso

É a combinação de try? e compactMap, as duas ferramentas apresentadas anteriormente. Ela expressa como um tipo a política de “descartar um item inválido e manter o restante”. Mas atenção: esse padrão engole as falhas silenciosamente. Em produção, registre a quantidade de falhas para que problemas nos dados do servidor não fiquem escondidos.

Ilustração de uma máquina de triagem que envia apenas um item defeituoso para uma caixa nil e deixa os demais passar
Descarte um item defeituoso e mantenha o restante, mas registre o que foi descartado

Depuração — DecodingError já sabe a resposta

Por fim, veja como investigar uma falha de decodificação. Capturar try decoder.decode(...) captura DecodingError, que é mais útil do que parece. Cada um dos quatro casos (keyNotFound, typeMismatch, valueNotFound e dataCorrupted) informa qual chave falhou, em qual codingPath, o que era esperado e o que chegou.

Quando a decodificação falhar, crie o hábito de inspecionar print(error) em vez de print(error as? DecodingError); pelo menos durante o desenvolvimento, imprima o codingPath por caso no bloco catch. Isso reduz muito o tempo de depuração. Na maioria dos casos de “não consigo analisar o JSON”, a resposta já está dentro do erro.

Resumo

  • A mágica de Codable é a síntese do compilador. Ele escreve o enum CodingKeys e init(from:)/encode(to:) para você; personalizar significa decidir quanto substituir manualmente.
  • Chaves: com snake_case consistente, uma linha de keyDecodingStrategy; com nomes irregulares, declare CodingKeys manualmente. Remover um case exclui o campo da conversão.
  • Datas: especifique dateDecodingStrategy conforme o formato do servidor e tenha cuidado com o locale en_US_POSIX e com ISO 8601 com milissegundos.
  • Aninhamento: por padrão, espelhe o envelope; nivele apenas os modelos principais com init(from:) + nestedContainer.
  • Falha parcial: declaração honesta de optionals + padrão FailableItem (try? + compactMap), tornando visíveis por log as falhas engolidas.
  • Depuração: a resposta está no codingPath de DecodingError.

Na próxima parte, veremos o princípio da sintaxe com arroba, como @State e @Published, e os property wrappers. Vamos criar a “sintaxe que envolve propriedades armazenadas” anunciada na parte sobre propriedades.

Continue lendo