web-dev-qa-db-fra.com

Swift 4 JSON Décodable façon la plus simple de décoder le changement de type

Avec Swift 4, il existe un grand niveau de stratégies de conversion de données et de date sous le capot.

Étant donné le JSON:

{
    "name": "Bob",
    "age": 25,
    "tax_rate": "4.25"
}

Je veux le contraindre 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 date peut convertir une date basée sur une chaîne en une date.

Y a-t-il quelque chose qui le fait avec un flotteur basé sur une chaîne

Sinon, je suis resté coincé à l'aide de CodingKey pour introduire une chaîne et utiliser un get informatique:

    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 fait faire plus d'entretien qu'il n'y paraît.

Est-ce la manière la plus simple ou existe-t-il quelque chose de similaire à DateDecodingStrategy pour les autres conversions de type?

Mise à jour: Je devrais noter: j'ai également suivi la voie de la priorité

init(from decoder:Decoder)

Mais c'est dans la direction opposée car cela m'oblige à tout faire pour moi.

24
Dru Freeman

Malheureusement, je ne pense pas qu'une telle option existe dans l'API JSONDecoder actuelle. Il n'existe qu'une option pour convertir des valeurs à virgule flottante exceptionnelles vers et depuis une représentation sous forme de chaîne.

Une autre solution possible au décodage manuel consiste à définir un type d'encapsuleur Codable pour tout LosslessStringConvertible qui peut coder et décoder à partir de sa représentation String:

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 simplement avoir une propriété de ce type et utiliser la conformité Codable générée automatiquement:

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, vous devez maintenant parler en termes de taxRate.decoded afin d'interagir avec la valeur Float.

Cependant, vous pouvez toujours définir une propriété calculée de transfert simple pour atténuer cela:

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 simple qu'il devrait l'être - avec un peu de chance, une version ultérieure de l'API JSONDecoder inclura plus d'options de décodage personnalisées, ou bien aura la possibilité d'exprimer des conversions de type dans le Codable API elle-même.

Cependant, l'un des avantages de la création du type wrapper est qu'il peut également être utilisé pour simplifier le décodage et l'encodage manuels. Par exemple, avec un 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
    }
}
18
Hamish

Vous pouvez toujours décoder manuellement. Donc, étant donné:

{
    "name": "Bob",
    "age": 25,
    "tax_rate": "4.25"
}

Tu peux 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 manuellement dans Encodage et décodage des types personnalisés .

Mais je suis d'accord, il semble qu'il devrait y avoir un processus de conversion de chaîne plus élégant équivalent à DateDecodingStrategy étant donné le nombre de sources JSON qui renvoient incorrectement des valeurs numériques sous forme de chaînes.

14
Rob

Selon vos besoins, vous pouvez choisir l'une des deux manières suivantes afin de résoudre votre problème.


#1. Utilisation de Decodableinit(from:) initializer

Utilisez cette stratégie lorsque vous devez convertir de String en Float pour une seule structure, énumération 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
    }

}

tilisation:

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 plusieurs clés imbriquées dans votre JSON ou lorsque vous devez convertir de nombreuses clés (par exemple de String à Float) à partir 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
    }

}

tilisation:

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
 */
7
Imanou Petit

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 une constante let si vous voulez accéder à taxRate, car la première fois que vous y accédez, vous mutez la structure.

// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
1
Code Different

Je sais que c'est une réponse vraiment tardive, mais j'ai commencé à travailler sur Codable il y a 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 en KeyedDecodingContainer et appeler la méthode dans l'extension depuis init(from decoder: Decoder){}

Pour le problème mentionné dans ce numéro, consultez l'extension que j'ai écrite 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 à partir de la méthode init(from decoder: Decoder). 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 approche pour convertir tout type de données en tout 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érerai cette approche à JSONEncoder().dateEncodingStrategy car si vous l'écrivez correctement, vous pouvez inclure différents formats de date dans la même réponse.

J'espère que j'ai aidé.

1
Suran