Encoder la valeur nil comme null avec JSONEncoder

J'utilise Swift 4 JSONEncoder. J'ai une structure Codable avec une propriété facultative, et je voudrais que cette propriété apparaisse comme null Valeur dans les données JSON produites lorsque la valeur est nil. Cependant, JSONEncoder supprime la propriété et ne l'ajoute pas à la sortie JSON. Existe-t-il un moyen de configurer JSONEncoder afin qu'il préserve la clé et la définisse sur 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)!)
24
demandé sur Paulo Mattos 2017-11-13 17:38:18

2 réponses

Oui, mais vous devrez écrire votre propre encodeur; vous ne pouvez pas utiliser celui par défaut.

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

Encoder un optionnel directement encodera un null, comme vous le cherchez.

Si c'est une utilisation importante de cas pour vous, vous pouvez envisager l'ouverture d'un défaut à bugs.swift.org pour demander un nouveau OptionalEncodingStrategy drapeau pour être ajouté sur JSONEncoder pour correspondre à l'actuel DateEncodingStrategy, etc. (Voir ci-dessous pourquoi cela est probablement impossible à implémenter dans Swift aujourd'hui, mais entrer dans le suivi le système est toujours utile à mesure que Swift évolue.)


Edit: aux questions de Paulo ci-dessous, cela envoie à la version Générique encode<T: Encodable> parce que 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 enveloppe l'appel à encodeNil, et je pense que laisser stdlib Gérer les options comme un autre Encodable est mieux que de les traiter comme un cas particulier dans notre propre encodeur et d'appeler encodeNil nous-mêmes.

Une Autre question évidente est pourquoi il fonctionne de cette façon en premier lieu. Puisque Optional est Encodable et que la conformité Encodable générée Code toutes les propriétés, pourquoi "encoder toutes les propriétés à la main" fonctionne-t-elle différemment? La réponse est que le générateur de conformité inclut un cas particulier 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 la modification de 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 le Swift....)

17
répondu Rob Napier 2017-11-13 16:30:21

Je suis tombé sur 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'attributs de table côté serveur sont égaux à vos noms de propriétés struct). Dans ce cas, utilisez simplement le " nom " du miroir.enfant.

Si vous avez besoin de CodingKeys, n'oubliez pas d'ajouter le protocoleCaseIterable . Qui rend possible l'utilisation d' la variableallCases .

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

L'extension de tableau est nécessaire si vous voulez créer un tableau de dictionnaires MyStruct.

0
répondu guido 2018-08-30 22:27:04