web-dev-qa-db-fra.com

Encode une valeur nulle comme nulle avec JSONEncoder

J'utilise Swift 4 JSONEncoder. J'ai une structure Codable avec une propriété facultative, et j'aimerais que cette propriété apparaisse comme null valeur dans les données JSON produites lorsque la valeur est nil. Cependant, JSONEncoder ignore la propriété et ne l'ajoute pas à la sortie JSON. Existe-t-il un moyen de configurer JSONEncoder pour qu'il conserve la clé et la mette à null dans ce cas?

Exemple

L'extrait de code ci-dessous produit {"number":1}, mais je préfère qu'il me donne {"string":null,"number":1}:

struct Foo: Codable {
  var string: String? = nil
  var number: Int = 1
}

let encoder = JSONEncoder()
let data = try! encoder.encode(Foo())
print(String(data: data, encoding: .utf8)!)
35
dr_barto

Oui, mais vous devrez écrire votre propre implémentation encode(to:), vous ne pouvez pas utiliser celle générée automatiquement.

struct Foo: Codable {
    var string: String? = nil
    var number: Int = 1

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(number, forKey: .number)
        try container.encode(string, forKey: .string)
    }
}

L'encodage d'un optionnel directement encodera un null, comme vous le recherchez.

S'il s'agit d'un cas d'utilisation important pour vous, vous pouvez envisager d'ouvrir un défaut sur bugs.Swift.org pour demander qu'un nouveau drapeau OptionalEncodingStrategy soit ajouté sur JSONEncoder pour correspondre à l'existant. DateEncodingStrategy, etc. (Voir ci-dessous pourquoi il est probablement impossible de l'implémenter réellement dans Swift aujourd'hui, mais entrer dans le système de suivi est toujours utile comme Swift évolue.)


Edit: Aux questions de Paulo ci-dessous, cela envoie au générique encode<T: Encodable> version car Optional est conforme à Encodable. Ceci est implémenté dans Codable.Swift de cette façon:

extension Optional : Encodable /* where Wrapped : Encodable */ {
    @_inlineable // FIXME(sil-serialize-all)
    public func encode(to encoder: Encoder) throws {
        assertTypeIsEncodable(Wrapped.self, in: type(of: self))

        var container = encoder.singleValueContainer()
        switch self {
        case .none: try container.encodeNil()
        case .some(let wrapped): try (wrapped as! Encodable).__encode(to: &container)
        }
    }
}

Cela encapsule l'appel à encodeNil, et je pense que laisser stdlib gérer les options comme juste un autre Encodable est mieux que de les traiter comme un cas spécial dans notre propre encodeur et d'appeler encodeNil nous-mêmes.

Une autre question évidente est pourquoi cela fonctionne de cette façon en premier lieu. Puisque Facultatif est Encodable et que la conformité Encodable générée code toutes les propriétés, pourquoi "coder toutes les propriétés à la main" fonctionne-t-il différemment? La réponse est que le générateur de conformité inclut un cas spécial pour les options :

// Now need to generate `try container.encode(x, forKey: .x)` for all
// existing properties. Optional properties get `encodeIfPresent`.
...

if (varType->getAnyNominal() == C.getOptionalDecl() ||
    varType->getAnyNominal() == C.getImplicitlyUnwrappedOptionalDecl()) {
  methodName = C.Id_encodeIfPresent;
}

Cela signifie que changer ce comportement nécessiterait de changer la conformité générée automatiquement, pas JSONEncoder (ce qui signifie également qu'il est probablement très difficile de rendre configurable dans Swift d'aujourd'hui ...)

29
Rob Napier

J'ai rencontré le même problème. Résolu en créant un dictionnaire à partir de la structure sans utiliser JSONEncoder. Vous pouvez le faire d'une manière relativement universelle. Voici mon code:

struct MyStruct: Codable {
    let id: String
    let regionsID: Int?
    let created: Int
    let modified: Int
    let removed: Int?


    enum CodingKeys: String, CodingKey, CaseIterable {
        case id = "id"
        case regionsID = "regions_id"
        case created = "created"
        case modified = "modified"
        case removed = "removed"
    }

    var jsonDictionary: [String : Any] {
        let mirror = Mirror(reflecting: self)
        var dic = [String: Any]()
        var counter = 0
        for (name, value) in mirror.children {
            let key = CodingKeys.allCases[counter]
            dic[key.stringValue] = value
            counter += 1
        }
        return dic
    }
}

extension Array where Element == MyStruct {
    func jsonArray() -> [[String: Any]] {
        var array = [[String:Any]]()
        for element in self {
            array.append(element.jsonDictionary)
        }
        return array
    }
}

Vous pouvez le faire sans CodingKeys (si les noms d'attribut de table côté serveur sont égaux à vos noms de propriété struct). Dans ce cas, utilisez simplement le "nom" de mirror.children.

Si vous avez besoin de CodingKeys, n'oubliez pas d'ajouter le protocole CaseIterable. Cela permet d'utiliser la variable allCases.

Soyez prudent avec les structures imbriquées: par exemple. si vous avez une propriété avec une structure personnalisée comme type, vous devez également la convertir en dictionnaire. Vous pouvez le faire dans la boucle for.

L'extension Array est requise si vous souhaitez créer un tableau de dictionnaires MyStruct.

0
guido