
O formato JSON ganhou grande popularidade, geralmente é usado para transferência de dados e execução de consultas em aplicativos cliente-servidor. A análise JSON requer ferramentas de codificação / decodificação desse formato, e a Apple atualizou-as recentemente. Neste artigo, examinaremos os métodos de análise JSON usando o protocolo
Decodable , comparamos o novo protocolo
Codable com o predecessor
NSCoding , avaliamos as vantagens e desvantagens, analisamos tudo com exemplos específicos e também consideramos alguns dos recursos encontrados ao implementar os protocolos.
O que é codável?Na WWDC2017, juntamente com a nova versão do Swift 4, a Apple introduziu novas ferramentas de codificação / decodificação de dados que são implementadas pelos três protocolos a seguir:
-
Codificável-
Codificável-
DecodívelNa maioria dos casos, esses protocolos são usados para trabalhar com JSON, mas também são usados para salvar dados em disco, transferir através da rede etc. Encodable é usado para converter estruturas de dados Swift em objetos JSON, enquanto Decodable, pelo contrário, ajuda a converter objetos JSON em modelos de dados Swift. O protocolo Codable combina os dois anteriores e é sua tipealias:
typealias Codable = Encodable & Decodable
Para estar em conformidade com esses protocolos, os tipos de dados devem implementar os seguintes métodos:
Codificávelcodificar (para :) - codifica o modelo de dados no tipo de codificador fornecido
Decodableinit (from :) - inicializa o modelo de dados a partir do decodificador fornecido
Codificávelcodificar (para :)
init (de :)
Um caso de uso simplesAgora considere um exemplo simples de uso do
Codable , já que ele implementa o
Encodable e
Decodable , neste exemplo, você pode ver imediatamente toda a funcionalidade do protocolo. Digamos que temos a estrutura de dados JSON mais simples:
{ "title": "Nike shoes", "price": 10.5, "quantity": 1 }
O modelo de dados para trabalhar com este JSON terá a seguinte aparência:
struct Product: Codable { var title:String var price:Double var quantity:Int enum CodingKeys: String, CodingKey { case title case price case quantity } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(price, forKey: .price) try container.encode(quantity, forKey: .quantity) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) price = try container.decode(Double.self, forKey: .price) quantity = try container.decode(Int.self, forKey: .quantity) } }
Os dois métodos necessários são implementados, a enumeração também é descrita para determinar a lista de campos de codificação / decodificação. De fato, a escrita pode ser bastante simplificada porque o
Codable suporta a
geração automática dos métodos de codificação (para :) e init (from :), bem como a enumeração necessária. Ou seja, nesse caso, você pode escrever a estrutura da seguinte maneira:
struct Product: Codable { var title:String var price:Double var quantity:Int }
Extremamente simples e minimalista. Apenas, não esqueça que uma gravação concisa não funcionará se:
- a
estrutura do seu modelo de dados é diferente daquela que você deseja codificar / decodificar-
pode ser necessário codificar / decodificar propriedades adicionais além das propriedades do seu modelo de dados-
Algumas propriedades do seu modelo de dados podem não suportar o protocolo Codable. Nesse caso, você precisará convertê-los de / para o protocolo Codable-
caso os nomes de variáveis no modelo de dados e os nomes de campo no contêiner não correspondam a vocêComo já consideramos a definição mais simples de um modelo de dados, vale a pena dar um pequeno exemplo de seu uso prático:
Portanto, em uma linha, você pode analisar a resposta do servidor no formato JSON:
let product: Product = try! JSONDecoder().decode(Product.self, for: data)
E o código a seguir, pelo contrário, criará um objeto JSON a partir do modelo de dados:
let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject)
Tudo é muito conveniente e rápido.
Tendo descrito corretamente os modelos de dados e os tornado
codificáveis , você pode literalmente codificar / decodificar dados em uma linha. Mas consideramos o modelo de dados mais simples que contém um pequeno número de campos de um tipo simples. Considere os possíveis problemas:
Nem todos os campos no modelo de dados são codificáveis.Para que seu modelo de dados implemente o protocolo
Codable, todos os campos do modelo devem suportar esse protocolo. Por padrão, o protocolo
Codable suporta os seguintes tipos de dados:
String, Int, Double, Data, URL .
Codable também suporta
Matriz, Dicionário, Opcional , mas apenas se eles contiverem tipos
Codificáveis . Se algumas propriedades do modelo de dados não corresponderem a
Codable , elas deverão ser trazidas para ele.
struct Pet: Codable { var name: String var age: Int var type: PetType enum CodingKeys: String, CodingKey { case name case age case type } init(from decoder: Decoder) throws { . . . } func encode(to encoder: Encoder) throws { . . . } }
Se em nosso
modelo de dados
Codable usamos um tipo personalizado, por exemplo, como
PetType , e queremos codificá-lo / decodificá-lo, ele também deve implementar seu init e codificar também.
O modelo de dados não corresponde aos campos JSONSe no seu modelo de dados 3 campos estiverem definidos, por exemplo, e no objeto JSON você obtiver 5 campos, 2 dos quais são adicionais aos 3, nada será alterado na análise, você apenas obtém seus 3 campos desses 5. Se o contrário acontecer situação e no objeto JSON haverá pelo menos um campo do modelo de dados; ocorrerá um erro em tempo de execução.
Se alguns campos puderem ser opcionais e ausentes periodicamente em um objeto JSON, nesse caso, é necessário torná-los opcionais:
class Product: Codable { var id: Int var productTypeId: Int? var art: String var title: String var description: String var price: Double var currencyId: Int? var brandId: Int var brand: Brand? }
Usando estruturas JSON mais complexasGeralmente, a resposta do servidor é uma matriz de entidades, ou seja, você solicita, por exemplo, uma lista de lojas e obtém uma resposta no formato:
{ "items": [ { "id": 1, "title": " ", "link": "https://www.youtube.com/watch?v=Myp6rSeCMUw", "created_at": 1497868174, "previewImage": "http://img.youtube.com/vi/Myp6rSeCMUw/mqdefault.jpg" }, { "id": 2, "title": " 2", "link": "https://www.youtube.com/watch?v=wsCEuNJmvd8", "created_at": 1525952040, "previewImage": "http://img.youtube.com/vi/wsCEuNJmvd8/mqdefault.jpg" } ] }
Nesse caso, você pode escrevê-lo e decodificá-lo simplesmente como uma matriz de entidades da
Loja .
struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] }
Neste exemplo, a função automática
init funcionará, mas se você quiser escrever a decodificação, precisará especificar o tipo decodificado como uma matriz:
self.items = try container.decode([Shop].self, forKey: .items)
A estrutura da
loja também deve implementar o protocolo
Decodable ,
respectivamente. struct Shop: Decodable { var id: Int? var title: String? var address: String? var shortAddress: String? var createdAt: Date? enum CodingKeys: String, CodingKey { case id case title case address case shortAddress = "short_address" case createdAt = "created_at" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try? container.decode(Int.self, forKey: .id) self.title = try? container.decode(String.self, forKey: .title) self.address = try? container.decode(String.self, forKey: .address) self.shortAddress = try? container.decode(String.self, forKey: .shortAddress) self.createdAt = try? container.decode(Date.self, forKey: .createdAt) } }
A análise dessa matriz de elementos ficará assim:
let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data)
Assim, você pode trabalhar facilmente com matrizes de modelos de dados e usá-los dentro de outros modelos.
Formato da dataNeste exemplo, há mais uma nuance, aqui encontramos o uso do tipo
Data pela primeira vez. Ao usar esse tipo, pode haver problemas com a codificação da data e, geralmente, esse problema é consistente com o back-end. O formato padrão é
.deferToDate :
struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo)
myDate ficará assim:
{ "date" : 519751611.12542897 }
Se precisarmos usar, por exemplo, o formato
.iso8601 , poderemos alterar facilmente o formato usando a propriedade
dateEncodingStrategy :
encoder.dateEncodingStrategy = .iso8601
Agora a data ficará assim:
{ "date" : "2017-06-21T15:29:32Z" }
Você também pode usar um formato de data personalizado ou até mesmo escrever seu próprio decodificador de data usando as seguintes opções de formatação:
.formatted (DateFormatter) - seu próprio formato de decodificador de data
.custom ((Date, Encoder) lança -> Void) - crie seu próprio formato de decodificação de data completamente
Analisando objetos aninhadosJá examinamos como você pode usar modelos de dados em outros modelos, mas às vezes é necessário analisar os campos JSON incluídos em outros campos sem usar um modelo de dados separado. O problema será mais claro se o considerarmos com um exemplo. Temos o seguinte JSON:
{ "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } }
Precisamos analisar os
campos "média" e
"número de votos" , isso pode ser resolvido de duas maneiras: crie um modelo de dados de classificação com dois campos e salve os dados nele, ou use
nestedContainer . Já discutimos o primeiro caso, e o uso do segundo será mais ou menos assim:
class Product: Decodable { var id: Int var art: String? var title: String? var votesCount: Int var averageRating: Double enum CodingKeys: String, CodingKey { case id case art case title case ratings } enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" } required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.id = try container.decode(Int.self, forKey: .id) self.art = try? container.decode(String.self, forKey: .art) self.title = try? container.decode(String.self, forKey: .title)
Ou seja, esse problema é resolvido com a criação de outro contêiner adicional usando
nestedContainter e sua análise adicional. Essa opção é conveniente se o número de campos aninhados não for tão grande, caso contrário, é melhor usar um modelo de dados adicional.
Incompatibilidade de nomes de campos JSON e propriedades do modelo de dadosSe você prestar atenção em como as enumerações são definidas em nossos modelos de dados, poderá ver que às vezes os elementos das enumerações recebem uma string que altera o valor padrão, por exemplo:
enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" }
Isso é feito para corresponder corretamente aos nomes das variáveis do modelo e dos campos JSON. Isso geralmente é necessário para campos cujo nome consiste em várias palavras e, no JSON, eles são separados por sublinhados. Em princípio, essa redefinição da enumeração é a mais popular e parece simples, mas mesmo assim a Apple apresentou uma solução mais elegante. Esse problema pode ser resolvido em uma linha usando
keyDecodingStrategy . Esse recurso apareceu no Swift 4.1
Digamos que você tenha um JSON do formulário:
let jsonString = """ [ { "name": "MacBook Pro", "screen_size": 15, "cpu_count": 4 }, { "name": "iMac Pro", "screen_size": 27, "cpu_count": 18 } ] """ let jsonData = Data(jsonString.utf8)
Vamos criar um modelo de dados para ele:
struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int }
As variáveis do modelo são registradas de acordo com o contrato, começam com uma letra minúscula e, em seguida, cada palavra começa com uma letra maiúscula (o chamado
camelCase ). Mas no JSON, os campos são escritos com sublinhados (o chamado
snake_case ). Agora, para que a análise seja bem-sucedida, precisamos definir uma enumeração no modelo de dados no qual estabeleceremos a correspondência dos nomes dos campos JSON com os nomes das variáveis ou obteremos um erro de tempo de execução. Mas agora é possível simplesmente definir
keyDecodingStrategy let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) }
Para a função de
codificação , é possível usar a transformação inversa:
encoder.keyEncodingStrategy = .convertToSnakeCase
Também é possível personalizar
keyDecodingStrategy usando o seguinte fechamento:
let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! }
Esta entrada, por exemplo, permite o uso do separador "-" para JSON. Um exemplo do JSON usado:
{ "first-Name": "Taylor", "last-Name": "Swift", "age": 28 }
Assim, uma definição de enumeração adicional geralmente pode ser evitada.
Tratamento de errosAo analisar JSON e converter dados de um formato para outro, os erros são inevitáveis; portanto, vejamos as opções para lidar com diferentes tipos de erros. Ao decodificar, os seguintes tipos de erros são possíveis:
- DecodingError.dataCorrupted (DecodingError.Context) - os dados estão corrompidos. Geralmente significa que os dados que você está tentando decodificar não correspondem ao formato esperado; por exemplo, em vez do JSON esperado, você recebeu um formato completamente diferente.
- DecodingError.keyNotFound (CodingKey, DecodingError.Context) - o campo solicitado não foi encontrado. Significa que o campo que você esperava receber está ausente
- DecodingError.typeMismatch (Any.Type, DecodingError.Context) - digite incompatibilidade. Quando o tipo de dados no modelo não corresponde ao tipo do campo recebido
- DecodingError.valueNotFound (Any.Type, DecodingError.Context) - valor ausente para um campo específico. O campo que você definiu no modelo de dados não pôde ser inicializado, provavelmente nos dados recebidos esse campo é nulo. Este erro ocorre apenas com campos não opcionais, se o campo não precisar ter um valor, não se esqueça de torná-lo opcional.
Ao codificar dados, é possível um erro:
EncodingError.invalidValue (Any.Type, DecodingError.Context) - falha ao converter o modelo de dados para um formato específico
Um exemplo de manipulação de erro ao analisar JSON:
do { let decoder = JSONDecoder() _ = try decoder.decode(businessReviewResponse.self, from: data) } catch DecodingError.dataCorrupted(let context) { print(DecodingError.dataCorrupted(context)) } catch DecodingError.keyNotFound(let key, let context) { print(DecodingError.keyNotFound(key,context)) } catch DecodingError.typeMismatch(let type, let context) { print(DecodingError.typeMismatch(type,context)) } catch DecodingError.valueNotFound(let value, let context) { print(DecodingError.valueNotFound(value,context)) } catch let error{ print(error) }
É claro que o processamento de erros é melhor colocar uma função separada, mas aqui, para maior clareza, a análise de erros é realizada juntamente com a análise. Por exemplo, a saída de erro, se não houver valor para o campo "produto", terá a seguinte aparência:
Comparação de Codable e NSCodingObviamente, o protocolo Codable é um grande avanço na codificação / decodificação de dados, mas o protocolo NSCoding existia antes dele. Vamos tentar compará-los e ver quais benefícios o Codable tem:
- Ao usar o protocolo NSCoding , o objeto deve ser uma subclasse de NSObject , o que implica automaticamente que nosso modelo de dados seja uma classe. Em Codable , não há necessidade de herança, respectivamente, o modelo de dados pode ser de classe e struct e enum .
- Se você precisar de funções separadas de codificação e decodificação, como, por exemplo, no caso de analisar dados JSON recebidos por meio da API, poderá usar apenas um protocolo Decodable . Ou seja, não há necessidade de implementar os métodos de inicialização ou codificação às vezes desnecessários.
- Codable pode gerar automaticamente os métodos init e codificar necessários, bem como a enumeração opcional CodingKeys . Obviamente, isso só funciona se você tiver campos simples na estrutura de dados; caso contrário, será necessária uma personalização adicional. Na maioria dos casos, especialmente para estruturas básicas de dados, você pode usar a geração automática, especialmente se redefinir keyDecodingStrategy , isso é conveniente e reduz algum código desnecessário.
Os
protocolos codificáveis, decodificáveis e
codificáveis nos permitiram dar outro passo em direção à conveniência da conversão de dados, surgiram novas ferramentas de análise mais flexíveis, a quantidade de código foi reduzida e parte dos processos de conversão foi automatizada. Os protocolos são implementados nativamente no Swift 4 e permitem reduzir o uso de bibliotecas de terceiros, como o
SwiftyJSON , mantendo a usabilidade. Os protocolos também permitem organizar adequadamente a estrutura do código, separando modelos de dados e métodos para trabalhar com eles em módulos separados.