
Le format JSON a gagné en popularité, il est généralement utilisé pour le transfert de données et l'exécution de requêtes dans les applications client-serveur. L'analyse JSON nécessite des outils d'encodage / décodage de ce format, et Apple les a récemment mis à jour. Dans cet article, nous examinerons les méthodes d'analyse JSON à l'aide du protocole
Decodable , comparerons le nouveau protocole
Codable avec le prédécesseur
NSCoding , évaluerons les avantages et les inconvénients, analyserons tout avec des exemples spécifiques et examinerons également certaines des fonctionnalités rencontrées lors de la mise en œuvre des protocoles.
Qu'est-ce que Codable?Lors de la WWDC2017, avec la nouvelle version de Swift 4, Apple a introduit de nouveaux outils de codage / décodage des données qui sont mis en œuvre par les trois protocoles suivants:
-
Codable-
Encodable-
DécodableDans la plupart des cas, ces protocoles sont utilisés pour fonctionner avec JSON, mais en plus, ils sont également utilisés pour enregistrer des données sur le disque, les transférer sur le réseau, etc. Encodable est utilisé pour convertir les structures de données Swift en objets JSON, tandis que Decodable, au contraire, aide à convertir les objets JSON en modèles de données Swift. Le protocole Codable combine les deux précédents et est leur typealias:
typealias Codable = Encodable & Decodable
Pour se conformer à ces protocoles, les types de données doivent implémenter les méthodes suivantes:
Encodableencode (to :) - encode le modèle de données dans le type d'encodeur donné
Décodableinit (from :) - initialise le modèle de données à partir du décodeur fourni
Codableencoder (à :)
init (de :)
Un cas d'utilisation simpleConsidérons maintenant un exemple simple d'utilisation de
Codable , car il implémente à la fois
Encodable et
Decodable , dans cet exemple, vous pouvez immédiatement voir toutes les fonctionnalités du protocole. Disons que nous avons la structure de données JSON la plus simple:
{ "title": "Nike shoes", "price": 10.5, "quantity": 1 }
Le modèle de données pour travailler avec ce JSON ressemblera à ceci:
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) } }
Les deux méthodes nécessaires sont implémentées, l'énumération est également décrite pour déterminer la liste des champs de codage / décodage. En fait, l'écriture peut être grandement simplifiée car
Codable prend en charge la
génération automatique des méthodes d'encodage (to :) et init (from :), ainsi que l'énumération nécessaire. Autrement dit, dans ce cas, vous pouvez écrire la structure comme suit:
struct Product: Codable { var title:String var price:Double var quantity:Int }
Extrêmement simple et minimaliste. Seulement, n'oubliez pas qu'un tel enregistrement concis ne fonctionnera pas si:
- la
structure de votre modèle de données est différente de celle que vous souhaitez encoder / décoder-
vous devrez peut-être coder / décoder des propriétés supplémentaires en plus des propriétés de votre modèle de données-
Certaines propriétés de votre modèle de données peuvent ne pas prendre en charge le protocole Codable. Dans ce cas, vous devrez les convertir de / vers le protocole codable-
au cas où les noms de variables dans le modèle de données et les noms de champs dans le conteneur ne vous correspondent pasPuisque nous avons déjà considéré la définition la plus simple d'un modèle de données, il convient de donner un petit exemple de son utilisation pratique:
Ainsi, en une seule ligne, vous pouvez analyser la réponse du serveur au format JSON:
let product: Product = try! JSONDecoder().decode(Product.self, for: data)
Et le code suivant, au contraire, créera un objet JSON à partir du modèle de données:
let productObject = Product(title: "Cheese", price: 10.5, quantity: 1) let encodedData = try? JSONEncoder().encode(productObject)
Tout est très pratique et rapide.
Après avoir correctement décrit les modèles de données et les avoir
codés , vous pouvez littéralement encoder / décoder les données sur une seule ligne. Mais nous avons considéré le modèle de données le plus simple contenant un petit nombre de champs d'un type simple. Considérez les problèmes possibles:
Tous les champs du modèle de données ne sont pas codables.Pour que votre modèle de données implémente le protocole
codable, tous les champs du modèle doivent prendre en charge ce protocole. Par défaut, le protocole
codable prend en charge les types de données suivants:
chaîne, entier, double, données, URL .
Codable prend également en charge
Array, Dictionary, Optional , mais uniquement s'ils contiennent des types
Codable . Si certaines propriétés du modèle de données ne correspondent pas à
Codable , elles doivent y être apportées.
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 { . . . } }
Si dans notre
modèle de données
codables nous utilisons un type personnalisé, par exemple, comme
PetType , et que nous voulons le coder / décoder, alors il doit également implémenter son init et son encodage.
Le modèle de données ne correspond pas aux champs JSONSi, dans votre modèle de données, 3 champs sont définis, par exemple, et dans l'objet JSON, vous obtenez 5 champs, dont 2 supplémentaires à ces 3, alors rien ne changera dans l'analyse, vous en retirerez simplement vos 3 champs 5. Si l'inverse se produit situation et dans l'objet JSON il y aura au moins un champ du modèle de données, une erreur d'exécution se produira.
Si certains champs peuvent être facultatifs et périodiquement absents dans un objet JSON, alors dans ce cas, il est nécessaire de les rendre facultatifs:
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? }
Utilisation de structures JSON plus complexesSouvent, la réponse du serveur est un tableau d'entités, c'est-à-dire que vous demandez, par exemple, une liste de magasins et obtenez une réponse sous la forme:
{ "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" } ] }
Dans ce cas, vous pouvez l'écrire et le décoder simplement comme un tableau d'entités
Shop .
struct ShopListResponse: Decodable { enum CodingKeys: String, CodingKey { case items } let items: [Shop] }
Dans cet exemple, la fonction automatique
init fonctionnera, mais si vous voulez écrire le décodage vous-même, vous devrez spécifier le type décodé sous forme de tableau:
self.items = try container.decode([Shop].self, forKey: .items)
La structure
Shop devrait également implémenter le protocole
Decodable ,
respectivement. 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) } }
L'analyse de ce tableau d'éléments ressemblera à ceci:
let parsedResult: ShopListResponse = try? JSONDecoder().decode(ShopListResponse.self, from: data)
Ainsi, vous pouvez facilement travailler avec des tableaux de modèles de données et les utiliser dans d'autres modèles.
Format de dateDans cet exemple, il y a une nuance de plus, ici nous avons rencontré pour la première fois l'utilisation du type
Date . Lorsque vous utilisez ce type, il peut y avoir des problèmes avec le codage de la date, et généralement ce problème est cohérent avec le backend. Le format par défaut est
.deferToDate :
struct MyDate : Encodable { let date: Date } let myDate = MyDate(date: Date()) try! encoder.encode(foo)
myDate ressemblera à ceci:
{ "date" : 519751611.12542897 }
Si nous devons utiliser, par exemple, le format
.iso8601 , nous pouvons facilement changer le format en utilisant la propriété
dateEncodingStrategy :
encoder.dateEncodingStrategy = .iso8601
Maintenant, la date ressemblera à ceci:
{ "date" : "2017-06-21T15:29:32Z" }
Vous pouvez également utiliser un format de date personnalisé ou même écrire votre propre décodeur de date en utilisant les options de formatage suivantes:
.formatted (DateFormatter) - son propre format de décodeur de date
.custom ((Date, Encoder) lance -> Void) - créez complètement votre propre format de décodage de date
Analyse des objets imbriquésNous avons déjà examiné comment utiliser des modèles de données dans d'autres modèles, mais il est parfois nécessaire d'analyser les champs JSON inclus dans d'autres champs sans utiliser de modèle de données distinct. Le problème sera plus clair si nous le considérons avec un exemple. Nous avons le JSON suivant:
{ "id": 349, "art": "M0470500", "title": "- Vichy 50 ", "ratings": { "average_rating": 4.1034, "votes_count": 29 } }
Nous devons analyser les
champs «average» et
«votes_count» , cela peut être résolu de deux manières, soit créer un modèle de données de notation avec deux champs et y enregistrer les données, soit utiliser
nestedContainer . Nous avons déjà discuté du premier cas, et l'utilisation du second ressemblera à ceci:
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)
En d'autres
termes , ce problème est résolu en créant un autre conteneur supplémentaire à l'aide de
nestedContainter et de son analyse ultérieure. Cette option est pratique si le nombre de champs imbriqués n'est pas si grand, sinon il est préférable d'utiliser un modèle de données supplémentaire.
Non-concordance des noms de champ JSON et des propriétés du modèle de donnéesSi vous prêtez attention à la façon dont les énumérations sont définies dans nos modèles de données, vous pouvez voir que les éléments des énumérations reçoivent parfois une chaîne qui modifie la valeur par défaut, par exemple:
enum RatingsCodingKeys: String, CodingKey { case votesCount = "votes_count" case averageRating = "average_rating" }
Cette opération est effectuée afin de faire correspondre correctement les noms des variables de modèle et des champs JSON. Ceci est généralement requis pour les champs dont le nom se compose de plusieurs mots, et en JSON, ils sont séparés par des traits de soulignement. En principe, une telle redéfinition de l'énumération est la plus populaire et semble simple, mais même alors, Apple a proposé une solution plus élégante. Ce problème peut être résolu sur une seule ligne à l'aide de
keyDecodingStrategy . Cette fonctionnalité est apparue dans Swift 4.1
Disons que vous avez un JSON de la forme:
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)
Créons un modèle de données pour cela:
struct Mac: Codable { var name: String var screenSize: Int var cpuCount: Int }
Les variables du modèle sont enregistrées conformément à l'accord, commencent par une lettre minuscule, puis chaque mot commence par une majuscule (ce que l'on appelle
camelCase ). Mais en JSON, les champs sont écrits avec des
traits de soulignement (le soi-disant
snake_case ). Maintenant, pour que l'analyse réussisse, nous devons soit définir une énumération dans le modèle de données dans laquelle nous établirons la correspondance des noms des champs JSON avec les noms des variables, soit nous obtiendrons une erreur d'exécution. Mais maintenant, il est possible de simplement définir
keyDecodingStrategy let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { let macs = try decoder.decode([Mac].self, from: jsonData) } catch { print(error.localizedDescription) }
Pour la fonction d'
encodage , vous pouvez donc utiliser la transformation inverse:
encoder.keyEncodingStrategy = .convertToSnakeCase
Il est également possible de personnaliser
keyDecodingStrategy en utilisant la fermeture suivante:
let jsonDecoder = JSONDecoder() jsonDecoder.keyDecodingStrategy = .custom { keys -> CodingKey in let key = keys.last!.stringValue.split(separator: "-").joined() return PersonKey(stringValue: String(key))! }
Cette entrée, par exemple, permet d'utiliser le séparateur "-" pour JSON. Un exemple de JSON utilisé:
{ "first-Name": "Taylor", "last-Name": "Swift", "age": 28 }
Ainsi, une définition supplémentaire d'une énumération peut souvent être évitée.
Gestion des erreursLors de l'analyse JSON et de la conversion de données d'un format à un autre, les erreurs sont inévitables, alors examinons les options pour gérer différents types d'erreurs. Lors du décodage, les types d'erreurs suivants sont possibles:
- DecodingError.dataCorrupted (DecodingError.Context) - les données sont corrompues. Signifie généralement que les données que vous essayez de décoder ne correspondent pas au format attendu, par exemple, au lieu du JSON attendu, vous avez reçu un format complètement différent.
- DecodingError.keyNotFound (CodingKey, DecodingError.Context) - le champ demandé est introuvable. Signifie que le champ que vous vous attendiez à recevoir est manquant
- DecodingError.typeMismatch (Any.Type, DecodingError.Context) - incompatibilité de type. Lorsque le type de données dans le modèle ne correspond pas au type du champ reçu
- DecodingError.valueNotFound (Any.Type, DecodingError.Context) - valeur manquante pour un champ spécifique. Le champ que vous avez défini dans le modèle de données n'a pas pu être initialisé, probablement dans les données reçues ce champ est nul. Cette erreur se produit uniquement avec les champs non facultatifs, si le champ ne doit pas avoir de valeur, n'oubliez pas de le rendre facultatif.
Lors de l'encodage des données, une erreur est possible:
EncodingError.invalidValue (Any.Type, DecodingError.Context) - n'a pas réussi à convertir le modèle de données dans un format spécifique
Un exemple de gestion des erreurs lors de l'analyse 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) }
Le traitement des erreurs est bien sûr préférable de mettre dans une fonction distincte, mais ici, pour plus de clarté, l'analyse des erreurs est effectuée avec l'analyse. Par exemple, la sortie d'erreur s'il n'y a pas de valeur pour le champ «produit» ressemblera à ceci:
Comparaison de Codable et NSCodingBien sûr, le protocole Codable est un grand pas en avant dans le codage / décodage des données, mais le protocole NSCoding existait avant lui. Essayons de les comparer et voyons quels sont les avantages de Codable:
- Lorsque vous utilisez le protocole NSCoding , l'objet doit être une sous-classe de NSObject , ce qui implique automatiquement que notre modèle de données doit être une classe. Dans Codable , il n'y a pas besoin d'héritage, respectivement, le modèle de données peut être à la fois class, struct et enum .
- Si vous avez besoin de fonctions d'encodage et de décodage distinctes, comme, par exemple, dans le cas de l'analyse des données JSON reçues via l'API, vous ne pouvez utiliser qu'un seul protocole décodable . Autrement dit, il n'est pas nécessaire d'implémenter les méthodes d' initialisation ou de codage parfois inutiles.
- Codable peut générer automatiquement les méthodes d' initialisation et de codage requises, ainsi que l'énumération facultative CodingKeys . Bien sûr, cela ne fonctionne que si vous avez des champs simples dans la structure de données, sinon, une personnalisation supplémentaire sera nécessaire. Dans la plupart des cas, en particulier pour les structures de données de base, vous pouvez utiliser la génération automatique, surtout si vous redéfinissez keyDecodingStrategy , cela est pratique et réduit le code inutile.
Les
protocoles Codable, Decodable et
Encodable nous ont permis de franchir une nouvelle étape vers la commodité de la conversion des données, de nouveaux outils d'analyse plus flexibles sont apparus, la quantité de code a été réduite et une partie des processus de conversion a été automatisée. Les protocoles sont implémentés nativement dans Swift 4 et permettent de réduire l'utilisation de bibliothèques tierces, telles que
SwiftyJSON , tout en conservant la convivialité. Les protocoles permettent également d'organiser correctement la structure du code en séparant les modèles de données et les méthodes de travail avec eux dans des modules séparés.