Swift 4 JSON décodable la façon la plus simple de décoder le changement de type
avec le protocole Codable de swift4 il y a un grand niveau de date sous le capot et des stratégies de conversion de données.
compte tenu de l'JSON:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
je veux forcer dans la structure suivante
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
la stratégie de décodage de la Date peut convertir une date basée sur une chaîne en une Date.
y a-t-il quelque chose qui fait cela avec un flotteur basé sur une chaîne de caractères
sinon j'ai été coincé avec L'utilisation CodingKey pour apporter une chaîne et utiliser un le calcul de l'obtenir:
enum CodingKeys: String, CodingKey {
case name, age
case sTaxRate = "tax_rate"
}
var sTaxRate: String
var taxRate: Float { return Float(sTaxRate) ?? 0.0 }
ce genre de brins me faire plus d'entretien qu'il ne semble devrait être nécessaire.
Est-ce la manière la plus simple ou y a-t-il quelque chose de similaire à DateDecodingStrategy pour d'autres conversions de type?
mise à Jour: je note: j'ai également opté pour la voie de l'annulation de la
init(from decoder:Decoder)
mais c'est dans la direction opposée car cela me force à tout faire pour moi-même.
6 réponses
Malheureusement, je ne crois pas qu'une telle option existe dans le courant JSONDecoder
API. Il n'existe qu'une option pour conversion exceptionnel valeurs à virgule flottante vers et à partir d'une représentation string.
une autre solution possible au décodage manuel est de définir un Codable
type de wrapper pour tout LosslessStringConvertible
qui peut encoder et décoder de son String
représentation:
struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {
var decoded: Decoded
init(_ decoded: Decoded) {
self.decoded = decoded
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}
self.decoded = decoded
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}
Ensuite, vous pouvez juste avoir une propriété de ce type et utiliser l'auto-généré Codable
conformité:
struct Example : Codable {
var name: String
var age: Int
var taxRate: StringCodableMap<Float>
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
bien que malheureusement, maintenant vous devez parler en termes de taxRate.decoded
afin d'interagir avec le Float
valeur.
cependant vous pouvez toujours définir une simple redirection d'une propriété calculée afin d'alléger ceci:
struct Example : Codable {
var name: String
var age: Int
private var _taxRate: StringCodableMap<Float>
var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}
private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}
bien que ce ne soit toujours pas aussi astucieux qu'il devrait l'être – avec un peu de chance une version ultérieure du JSONDecoder
L'API inclura plus d'options de décodage personnalisées, ou bien avoir la capacité d'exprimer des conversions de type dans le Codable
API lui-même.
cependant un avantage de créer le type de wrapper est qu'il peut aussi être utilisé pour rendre le décodage manuel et le codage plus simple. Par exemple, avec le décodage manuel:
struct Example : Decodable {
var name: String
var age: Int
var taxRate: Float
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}
Vous pouvez toujours décoder manuellement. Donc, étant donné:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
Vous pouvez faire:
struct Example: Codable {
let name: String
let age: Int
let taxRate: Float
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
age = try values.decode(Int.self, forKey: .age)
guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
}
taxRate = rate
}
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
Voir Encoder et décoder manuellementencodage et décodage des types personnalisés.
mais je suis d'accord, qu'il semble qu'il devrait y avoir un processus de conversion de chaîne plus élégant équivalent à DateDecodingStrategy
étant donné combien de sources JSON là-bas renvoient incorrectement des valeurs numériques sous forme de chaînes.
en Fonction de vos besoins, vous pouvez choisir l'une des deux manières suivantes afin de résoudre votre problème.
#1. En utilisant Decodable
init(from:)
initialisation
utilisez cette stratégie lorsque vous avez besoin de convertir de String
Float
pour une seule structure, enum ou classe.
import Foundation
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
age = try container.decode(Int.self, forKey: CodingKeys.age)
let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
guard let taxRateFloat = Float(taxRateString) else {
let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = taxRateFloat
}
}
Utilisation:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#2. Utilisation d'un modèle intermédiaire
utilisez cette stratégie lorsque vous avez beaucoup de clés imbriquées dans votre JSON ou quand vous avez besoin de convertir beaucoup de clés (par exemple de String
Float
) de votre JSON.
import Foundation
fileprivate struct PrivateExampleJson: Decodable {
var name: String
var age: Int
var taxRate: String
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
init(from decoder: Decoder) throws {
let privateExampleJson = try PrivateExampleJson(from: decoder)
name = privateExampleJson.name
age = privateExampleJson.age
guard let convertedTaxRate = Float(privateExampleJson.taxRate) else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = convertedTaxRate
}
}
Utilisation:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
Vous pouvez utiliser lazy var
pour convertir la propriété en un autre type:
struct ExampleJson: Decodable {
var name: String
var age: Int
lazy var taxRate: Float = {
Float(self.tax_rate)!
}()
private var tax_rate: String
}
Un inconvénient de cette approche est que vous ne pouvez pas définir un let
constante si vous souhaitez accéder à taxRate
, depuis la première fois que vous y accédez, vous êtes en train de muter la structure.
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
je sais que c'est vraiment une réponse tardive, mais j'ai commencé à travailler sur Codable
quelques jours seulement. Et je suis tombé sur un problème similaire.
afin de convertir la chaîne en nombre flottant, vous pouvez écrire une extension KeyedDecodingContainer
et l'appel à la méthode dans l'extension de init(from decoder: Decoder){}
Pour le problème évoqué dans cette question, voir l'extension que j'ai écrit ci-dessous;
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
return Float(try decode(transformFrom, forKey: key))
}
}
vous pouvez appeler cette méthode de init(from decoder: Decoder)
méthode. Voir un exemple ci-dessous;
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
}
En fait, vous pouvez utiliser cette méthode pour convertir n'importe quel type de données d'un autre type. Vous pouvez convertir string to Date
,string to bool
,string to float
,float to int
etc.
en fait pour convertir une chaîne en objet Date, je préfère cette approche à JSONEncoder().dateEncodingStrategy
parce que si vous l'écrivez correctement, vous pouvez inclure différents formats de date dans la même réponse.
J'espère avoir aidé.
entrez la description du lien ici comment utiliser JSONDecodable dans Swift4
1) Obtenir la réponse JSON et créer Struct
2) classe de conform décodable dans la structure
3) autres étapes à suivre(exemple Simple)