web-dev-qa-db-fra.com

Avec JSONDecoder dans Swift 4, les clés manquantes peuvent-elles utiliser une valeur par défaut au lieu d’être des propriétés facultatives?

Swift 4 a ajouté le nouveau protocole Codeable. Lorsque j'utilise JSONDecoder, il semble que toutes les propriétés non facultatives de ma classe Codeable aient des clés dans le JSON, sinon une erreur est générée.

Rendre chaque propriété de ma classe facultative semble être un problème inutile puisque ce que je veux vraiment, c'est utiliser la valeur du json ou une valeur par défaut. (Je ne veux pas que la propriété soit nulle.)

Y a-t-il un moyen de faire cela?

class MyCodable: Codable {
    var name: String = "Default Appleseed"
}

func load(input: String) {
    do {
        if let data = input.data(using: .utf8) {
            let result = try JSONDecoder().decode(MyCodable.self, from: data)
            print("name: \(result.name)")
        }
    } catch  {
        print("error: \(error)")
        // `Error message: "Key not found when expecting non-optional type
        // String for coding key \"name\""`
    }
}

let goodInput = "{\"name\": \"Jonny Appleseed\" }"
let badInput = "{}"
load(input: goodInput) // works, `name` is Jonny Applessed
load(input: badInput) // breaks, `name` required since property is non-optional
52
zekel

Vous pouvez implémenter la méthode init(from decoder: Decoder) dans votre type au lieu d'utiliser l'implémentation par défaut:

class MyCodable: Codable {
    var name: String = "Default Appleseed"

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        }
    }
}

Vous pouvez également faire name une propriété constante (si vous le souhaitez):

class MyCodable: Codable {
    let name: String

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let name = try container.decodeIfPresent(String.self, forKey: .name) {
            self.name = name
        } else {
            self.name = "Default Appleseed"
        }
    }
}

ou 

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
}

Re votre commentaire: Avec une extension personnalisée 

extension KeyedDecodingContainer {
    func decodeWrapper<T>(key: K, defaultValue: T) throws -> T
        where T : Decodable {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultValue
    }
}

vous pouvez implémenter la méthode init comme

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decodeWrapper(key: .name, defaultValue: "Default Appleseed")
}

mais ce n'est pas beaucoup plus court que

    self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Default Appleseed"
71
Martin R

Une autre solution consisterait à utiliser une propriété calculée dont la valeur par défaut est la valeur souhaitée si la clé JSON n’est pas trouvée. Cela ajoute également un peu de verbosité supplémentaire car vous aurez besoin de déclarer une autre propriété et nécessitera l'ajout d'une énumération CodingKeys (si ce n'est déjà fait). L'avantage est que vous n'avez pas besoin d'écrire un code de décodage/encodage personnalisé.

Par exemple:

class MyCodable: Codable {
    var name: String { return _name ?? "Default Appleseed" }

    private var _name: String?

    enum CodingKeys: String, CodingKey {
        case _name = "name"
    }
}
7
Cristik

L’approche que je préfère utilise ce que l’on appelle des DTO - objets de transfert de données. C'est une structure qui se conforme à Codable et représente l'objet désiré.

struct MyClassDTO: Codable {
    let items: [String]?
    let otherVar: Int?
}

Ensuite, il vous suffit d'initier l'objet que vous souhaitez utiliser dans l'application avec ce DTO.

 class MyClass {
    let items: [String]
    var otherVar = 3
    init(_ dto: MyClassDTO) {
        items = dto.items ?? [String]()
        otherVar = dto.otherVar ?? 3
    }

    var dto: MyClassDTO {
        return MyClassDTO(items: items, otherVar: otherVar)
    }
}

Cette approche est également utile car vous pouvez renommer et modifier l’objet final comme vous le souhaitez. Il est clair et nécessite moins de code que le décodage manuel. De plus, avec cette approche, vous pouvez séparer la couche réseau des autres applications.

3
Leonid Silver

Si vous pensez que l'écriture de votre propre version de init(from decoder: Decoder) est écrasante, je vous conseillerais d'implémenter une méthode qui vérifiera l'entrée avant de l'envoyer au décodeur. De cette façon, vous aurez un endroit où vous pourrez vérifier l’absence de champs et définir vos propres valeurs par défaut.

Par exemple:

final class CodableModel: Codable
{
    static func customDecode(_ obj: [String: Any]) -> CodableModel?
    {
        var validatedDict = obj
        let someField = validatedDict[CodingKeys.someField.stringValue] ?? false
        validatedDict[CodingKeys.someField.stringValue] = someField

        guard
            let data = try? JSONSerialization.data(withJSONObject: validatedDict, options: .prettyPrinted),
            let model = try? CodableModel.decoder.decode(CodableModel.self, from: data) else {
                return nil
        }

        return model
    }

    //your coding keys, properties, etc.
}

Et pour initier un objet à partir de json, au lieu de:

do {
    let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted)
    let model = try CodableModel.decoder.decode(CodableModel.self, from: data)                        
} catch {
    assertionFailure(error.localizedDescription)
}

Init ressemblera à ceci:

if let vuvVideoFile = PublicVideoFile.customDecode($0) {
    videos.append(vuvVideoFile)
}

Dans cette situation particulière, je préfère traiter les options, mais si vous avez un avis différent, vous pouvez rendre votre méthode customDecode (:)

0
Eugene Alexeev

Vous pouvez implémenter.

struct Source : Codable {

    let id : String?
    let name : String?

    enum CodingKeys: String, CodingKey {
        case id = "id"
        case name = "name"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
        name = try values.decodeIfPresent(String.self, forKey: .name)
    }
}
0
Ankit

Si vous ne souhaitez pas implémenter vos méthodes d'encodage et de décodage, il existe une solution un peu sale pour les valeurs par défaut.

Vous pouvez déclarer votre nouveau champ facultatif implicitement non enveloppé et vérifier s'il est nul après le décodage et définir une valeur par défaut.

J'ai testé cela uniquement avec PropertyListEncoder, mais je pense que JSONDecoder fonctionne de la même manière.

0
Kirill Kuzyk