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.

17
demandé sur Lord Andrei 2017-06-16 20:11:03

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
    }
}
13
répondu Hamish 2017-10-20 13:36:15

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.

12
répondu Rob 2017-10-17 22:30:04

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 Decodableinit(from:) initialisation

utilisez cette stratégie lorsque vous avez besoin de convertir de StringFloat 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 StringFloat) 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
 */
6
répondu Imanou Petit 2018-04-02 21:09:17

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)
1
répondu Code Different 2017-06-16 23:25:17

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é.

1
répondu Suran 2018-07-09 13:05:43

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)

-1
répondu Ananda Aiwale 2018-05-01 17:38:58