Generic JSONDecoder

À l'heure actuelle, la grande majorité des applications mobiles sont client-serveur. Partout où il y a chargement, synchronisation, envoi d'événements et la principale façon d'interagir avec le serveur est d'échanger des données en utilisant le format json.


Décodage clé


La Fondation dispose de deux mécanismes de sérialisation des données mortes. L'ancien est NSJsonSerialization et le nouveau est Codable . Le dernier de la liste des avantages contient une chose aussi merveilleuse que la génération automatique de clés pour les données json basée sur une structure (ou classe) qui implémente Codable ( Encodable , Decodable ) et un initialiseur pour décoder les données.


Et tout semble aller bien, vous pouvez l'utiliser et en profiter, mais la réalité n'est pas si simple.
Assez souvent sur le serveur, vous pouvez voir json du formulaire:


 {"topLevelObject": { "underlyingObject": 1 }, "Error": { "ErrorCode": 400, "ErrorDescription": "SomeDescription" } } 

Il s'agit d'un exemple presque réel de l'un des serveurs du projet.


Pour la classe JsonDecoder , JsonDecoder pouvez spécifier le travail avec les clés snake_case, mais que faire si nous avons UpperCamelCase, dash-snake-case ou même un méli-mélo général, mais nous ne voulons pas écrire les clés manuellement?


Heureusement, Apple a fourni la possibilité de configurer le mappage des clés avant de le faire correspondre avec la structure JSONDecoder.KeyDecodingStrategy aide de JSONDecoder.KeyDecodingStrategy . Nous en profiterons.


Tout d'abord, créez une structure qui implémente le protocole CodingKey , car il n'y en a pas dans la bibliothèque standard:


  struct AnyCodingKey: CodingKey { var stringValue: String var intValue: Int? init(_ base: CodingKey) { self.init(stringValue: base.stringValue, intValue: base.intValue) } init(stringValue: String) { self.stringValue = stringValue } init(intValue: Int) { self.stringValue = "\(intValue)" self.intValue = intValue } init(stringValue: String, intValue: Int?) { self.stringValue = stringValue self.intValue = intValue } } 

Ensuite, il est nécessaire de traiter séparément chaque cas de nos clés. Les principaux:
snake_case, dash-snake-case, lowerCamelCase et UpperCamelCase. Vérifiez, exécutez, tout fonctionne.


Ensuite, nous rencontrons un problème plutôt attendu: les abréviations dans camelCase'ah (rappelez-vous les nombreux id , Id , ID ). Pour le faire fonctionner, vous devez les convertir correctement et introduire une règle - les abréviations sont converties en camelCase, en ne gardant que la première lettre majuscule et myABBRKey se transformera en myAbbrKey .


Cette solution fonctionne très bien pour les combinaisons de plusieurs cas.


Remarque: L' implémentation sera fournie dans. Stratégie de décodage des clés .custom .


 static func convertToProperLowerCamelCase(keys: [CodingKey]) -> CodingKey { guard let last = keys.last else { assertionFailure() return AnyCodingKey(stringValue: "") } if let fromUpper = convertFromUpperCamelCase(initial: last.stringValue) { return AnyCodingKey(stringValue: fromUpper) } else if let fromSnake = convertFromSnakeCase(initial: last.stringValue) { return AnyCodingKey(stringValue: fromSnake) } else { return AnyCodingKey(last) } } 

Décodage de la date


Le prochain problème de routine est la façon dont les dates sont passées. Il y a beaucoup de microservices sur le serveur, il y a un peu moins de commandes, mais aussi une quantité décente, et en conséquence nous sommes confrontés à un tas de formats de date comme «Oui, j'utilise la norme». De plus, quelqu'un passe des dates dans une chaîne, quelqu'un à l'époque de l'époque. En conséquence, nous avons à nouveau un méli-mélo de combinaisons chaîne-nombre-fuseau horaire-milliseconde-séparateur, et DateDecoder dans iOS se plaint et nécessite un format de date strict. La solution ici est simple, juste en recherchant nous recherchons des signes d'un format particulier et les combinons, obtenant le résultat nécessaire. Ces formats ont couvert avec succès et complètement mes cas.


Remarque: Il s'agit d'un initialiseur DateFormatter personnalisé. Son format juste défini pour le formateur créé.


 static let onlyDate = DateFormatter(format: "yyyy-MM-dd") static let full = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSSx") static let noWMS = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ssZ") static let noWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss.SSS") static let noWMSnoWTZ = DateFormatter(format: "yyyy-MM-dd'T'HH:mm:ss") 

Nous attachons cela à notre décodeur à l'aide de JSONDecoder.DateDecodingStrategy et obtenons un décodeur qui traite presque tout et le convertit en un format qui est digeste pour nous.


Tests de performance


Des tests ont été effectués pour les chaînes json de taille 7944 octets.


Stratégie convertFromSnakeCasestratégie anyCodingKey
Absolu0,001700,00210
Relative81%100%

Comme nous pouvons le voir, le Decoder personnalisé Decoder 20% plus lent en raison de la vérification obligatoire de chaque clé dans json pour le besoin de transformation. Cependant, il s'agit d'une somme modique pour ne pas avoir à enregistrer explicitement les clés des structures de données en implémentant Codable . Le nombre de plaques de chaudière est très réduit dans le projet avec l'ajout de ce décodeur. Dois-je l'utiliser pour gagner du temps aux développeurs, mais dégrader les performances? Ça dépend de vous.


Exemple de code complet dans la bibliothèque github


Article en anglais

Source: https://habr.com/ru/post/fr449870/


All Articles