140 votes

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

Rendre chaque propriété de ma classe facultative semble être un problème inutile, car 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
 

161voto

Martin R Points 105727

Vous pouvez mettre en œuvre l' init(from decoder: Decoder) méthode de votre type, au lieu d'utiliser la valeur par défaut de mise en œuvre:

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é de la constante (si vous voulez):

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 pourriez mettre en œuvre 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"

49voto

Cristik Points 1

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'un CodingKeys enum (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"
    }
}
 

18voto

Ankit Points 1

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)
    }
}
 

0voto

Eugene Alexeev Points 316

Si vous pensez que le fait d'écrire votre propre version de init(from decoder: Decoder) est écrasante, je vous conseille de mettre en œuvre une méthode qui permettra de vérifier l'entrée avant de l'envoyer à un décodeur. De cette façon, vous aurez un endroit où vous pouvez vérifier les champs d'absence et de 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 dans l'ordre d'initialisation d'un objet 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, je préfère traiter avec les options, mais si vous avez une opinion différente, vous pouvez faire votre customDecode(:) la méthode de throwable

0voto

Kirill Kuzyk Points 11

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.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X