Comment puis-je utiliser le codable de Swift pour encoder dans un dictionnaire?

110

J'ai une structure qui implémente Swift 4 Codable. Existe-t-il un moyen intégré simple d'encoder cette structure dans un dictionnaire?

let struct = Foo(a: 1, b: 2)
let dict = something(struct)
// now dict is ["a": 1, "b": 2]
zoul
la source

Réponses:

231

Si cela ne vous dérange pas de déplacer un peu les données, vous pouvez utiliser quelque chose comme ceci:

extension Encodable {
  func asDictionary() throws -> [String: Any] {
    let data = try JSONEncoder().encode(self)
    guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
      throw NSError()
    }
    return dictionary
  }
}

Ou une variante optionnelle

extension Encodable {
  var dictionary: [String: Any]? {
    guard let data = try? JSONEncoder().encode(self) else { return nil }
    return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
  }
}

En supposant qu'il Fooest conforme Codableou vraiment, Encodablevous pouvez le faire.

let struct = Foo(a: 1, b: 2)
let dict = try struct.asDictionary()
let optionalDict = struct.dictionary

Si vous voulez aller dans l'autre sens ( init(any)), jetez un œil à ce Init un objet conforme à Codable avec un dictionnaire / tableau

Chris Mitchelmore
la source
22

Voici des implémentations simples de DictionaryEncoder/ DictionaryDecoderqui enveloppent JSONEncoder, JSONDecoderet JSONSerialization, qui gèrent également les stratégies d'encodage / décodage ...

class DictionaryEncoder {

    private let encoder = JSONEncoder()

    var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy {
        set { encoder.dateEncodingStrategy = newValue }
        get { return encoder.dateEncodingStrategy }
    }

    var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy {
        set { encoder.dataEncodingStrategy = newValue }
        get { return encoder.dataEncodingStrategy }
    }

    var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy {
        set { encoder.nonConformingFloatEncodingStrategy = newValue }
        get { return encoder.nonConformingFloatEncodingStrategy }
    }

    var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy {
        set { encoder.keyEncodingStrategy = newValue }
        get { return encoder.keyEncodingStrategy }
    }

    func encode<T>(_ value: T) throws -> [String: Any] where T : Encodable {
        let data = try encoder.encode(value)
        return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
    }
}

class DictionaryDecoder {

    private let decoder = JSONDecoder()

    var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy {
        set { decoder.dateDecodingStrategy = newValue }
        get { return decoder.dateDecodingStrategy }
    }

    var dataDecodingStrategy: JSONDecoder.DataDecodingStrategy {
        set { decoder.dataDecodingStrategy = newValue }
        get { return decoder.dataDecodingStrategy }
    }

    var nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy {
        set { decoder.nonConformingFloatDecodingStrategy = newValue }
        get { return decoder.nonConformingFloatDecodingStrategy }
    }

    var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy {
        set { decoder.keyDecodingStrategy = newValue }
        get { return decoder.keyDecodingStrategy }
    }

    func decode<T>(_ type: T.Type, from dictionary: [String: Any]) throws -> T where T : Decodable {
        let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
        return try decoder.decode(type, from: data)
    }
}

L'utilisation est similaire à JSONEncoder/ JSONDecoder

let dictionary = try DictionaryEncoder().encode(object)

et

let object = try DictionaryDecoder().decode(Object.self, from: dictionary)

Pour plus de commodité, j'ai mis tout cela dans un dépôt…  https://github.com/ashleymills/SwiftDictionaryCoding

Ashley Mills
la source
Merci beaucoup!, L'alternative serait d'utiliser l'héritage mais le site appelant ne pourrait pas déduire le type comme un dictionnaire car il y aurait 2 fonctions de types de retour différents.
user1046037
17

J'ai créé une bibliothèque appelée CodableFirebase et son objectif initial était de l'utiliser avec Firebase Database, mais elle fait en fait ce dont vous avez besoin: elle crée un dictionnaire ou tout autre type comme dans JSONDecodermais vous n'avez pas besoin de faire la double conversion ici comme vous le faites dans d'autres réponses. Cela ressemblerait donc à quelque chose comme:

import CodableFirebase

let model = Foo(a: 1, b: 2)
let dict = try! FirebaseEncoder().encode(model)
Noobass
la source
7

Je ne sais pas si c'est le meilleur moyen, mais vous pouvez certainement faire quelque chose comme:

struct Foo: Codable {
    var a: Int
    var b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }
}

let foo = Foo(a: 1, b: 2)
let dict = try JSONDecoder().decode([String: Int].self, from: JSONEncoder().encode(foo))
print(dict)
Lawliet
la source
8
Cela ne fonctionnerait que pour les structures avec toutes les propriétés du même type
Leo Dabus
1
J'ai juste essayé "let dict = try JSONDecoder (). Decode ([String: Int] .self, from: JSONEncoder (). Encode (foo))" et j'ai obtenu "Devrait décoder le dictionnaire <String, Any> mais j'ai trouvé un tableau à la place. " pourriez-vous aider pls
Itan Hant
6

let dict = try JSONSerialization.jsonObject(with: try JSONEncoder().encode(struct), options: []) as? [String: Any]

Ryan Collins
la source
6

Il n'y a aucun moyen intégré de le faire. Comme indiqué ci - dessus, si vous n'avez aucun problème de performances, vous pouvez accepter l' implémentation JSONEncoder+ JSONSerialization.

Mais je préférerais suivre la voie de la bibliothèque standard pour fournir un objet encodeur / décodeur.

class DictionaryEncoder {
    private let jsonEncoder = JSONEncoder()

    /// Encodes given Encodable value into an array or dictionary
    func encode<T>(_ value: T) throws -> Any where T: Encodable {
        let jsonData = try jsonEncoder.encode(value)
        return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
    }
}

class DictionaryDecoder {
    private let jsonDecoder = JSONDecoder()

    /// Decodes given Decodable type from given array or dictionary
    func decode<T>(_ type: T.Type, from json: Any) throws -> T where T: Decodable {
        let jsonData = try JSONSerialization.data(withJSONObject: json, options: [])
        return try jsonDecoder.decode(type, from: jsonData)
    }
}

Vous pouvez l'essayer avec le code suivant:

struct Computer: Codable {
    var owner: String?
    var cpuCores: Int
    var ram: Double
}

let computer = Computer(owner: "5keeve", cpuCores: 8, ram: 4)
let dictionary = try! DictionaryEncoder().encode(computer)
let decodedComputer = try! DictionaryDecoder().decode(Computer.self, from: dictionary)

J'essaye de force ici de raccourcir l'exemple. Dans le code de production, vous devez gérer les erreurs de manière appropriée.

5keeve
la source
4

Dans certains projets, j'utilise la réflexion rapide. Mais attention, les objets codables imbriqués, ne sont pas mappés là aussi.

let dict = Dictionary(uniqueKeysWithValues: Mirror(reflecting: foo).children.map{ ($0.label!, $0.value) })
Nikolay Kapustin
la source
2

Je pense vraiment qu'il y a une valeur à pouvoir simplement utiliser Codablepour encoder vers / à partir de dictionnaires, sans jamais avoir l'intention de frapper JSON / Plists / quoi que ce soit. Il existe de nombreuses API qui vous donnent simplement un dictionnaire ou attendent un dictionnaire, et il est agréable de pouvoir les échanger facilement avec des structures ou des objets Swift, sans avoir à écrire un code standard sans fin.

J'ai joué avec du code basé sur la source Foundation JSONEncoder.swift (qui implémente en fait l'encodage / décodage du dictionnaire en interne, mais ne l'exporte pas).

Le code peut être trouvé ici: https://github.com/elegantchaos/DictionaryCoding

C'est encore assez approximatif, mais je l'ai développé un peu pour que, par exemple, il puisse remplir les valeurs manquantes avec des valeurs par défaut lors du décodage.

Sam Deane
la source
2

J'ai modifié PropertyListEncoder du projet Swift en DictionaryEncoder, simplement en supprimant la sérialisation finale du dictionnaire au format binaire. Vous pouvez faire la même chose vous-même, ou vous pouvez prendre mon code de ici

Il peut être utilisé comme ceci:

do {
    let employeeDictionary: [String: Any] = try DictionaryEncoder().encode(employee)
} catch let error {
    // handle error
}
Marmoy
la source
0

J'ai écrit un rapide essentiel pour gérer ce (ne pas utiliser le protocole codable). Attention, il ne vérifie pas le type de valeurs et ne fonctionne pas de manière récursive sur les valeurs encodables.

class DictionaryEncoder {
    var result: [String: Any]

    init() {
        result = [:]
    }

    func encode(_ encodable: DictionaryEncodable) -> [String: Any] {
        encodable.encode(self)
        return result
    }

    func encode<T, K>(_ value: T, key: K) where K: RawRepresentable, K.RawValue == String {
        result[key.rawValue] = value
    }
}

protocol DictionaryEncodable {
    func encode(_ encoder: DictionaryEncoder)
}
Sid Mani
la source
0

Il n'y a pas de moyen simple de faire cela dans Codable. Vous devez implémenter le protocole Encodable / Decodable pour votre structure. Pour votre exemple, vous devrez peut-être écrire comme ci-dessous

typealias EventDict = [String:Int]

struct Favorite {
    var all:EventDict
    init(all: EventDict = [:]) {
        self.all = all
    }
}

extension Favorite: Encodable {
    struct FavoriteKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: FavoriteKey.self)

        for eventId in all {
            let nameKey = FavoriteKey(stringValue: eventId.key)!
            try container.encode(eventId.value, forKey: nameKey)
        }
    }
}

extension Favorite: Decodable {

    public init(from decoder: Decoder) throws {
        var events = EventDict()
        let container = try decoder.container(keyedBy: FavoriteKey.self)
        for key in container.allKeys {
            let fav = try container.decode(Int.self, forKey: key)
            events[key.stringValue] = fav
        }
        self.init(all: events)
    }
}
Kraming
la source
0

J'ai créé un pod ici https://github.com/levantAJ/AnyCodable pour faciliter le décodage et l' encodage [String: Any] et[Any]

pod 'DynamicCodable', '1.0'

Et vous pouvez décoder et encoder [String: Any]et[Any]

import DynamicCodable

struct YourObject: Codable {
    var dict: [String: Any]
    var array: [Any]
    var optionalDict: [String: Any]?
    var optionalArray: [Any]?

    enum CodingKeys: String, CodingKey {
        case dict
        case array
        case optionalDict
        case optionalArray
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        dict = try values.decode([String: Any].self, forKey: .dict)
        array = try values.decode([Any].self, forKey: .array)
        optionalDict = try values.decodeIfPresent([String: Any].self, forKey: .optionalDict)
        optionalArray = try values.decodeIfPresent([Any].self, forKey: .optionalArray)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(dict, forKey: .dict)
        try container.encode(array, forKey: .array)
        try container.encodeIfPresent(optionalDict, forKey: .optionalDict)
        try container.encodeIfPresent(optionalArray, forKey: .optionalArray)
    }
}
Tai Le
la source
1
Votre exemple ne montre pas comment résoudre le problème
Simon Moshenko
0

Si vous utilisez SwiftyJSON , vous pouvez faire quelque chose comme ceci:

JSON(data: JSONEncoder().encode(foo)).dictionaryObject

Remarque: vous pouvez également transmettre ce dictionnaire parametersaux requêtes Alamofire .

Jakub Truhlář
la source
0

Voici une solution basée sur un protocole:

protocol DictionaryEncodable {
    func encode() throws -> Any
}

extension DictionaryEncodable where Self: Encodable {
    func encode() throws -> Any {
        let jsonData = try JSONEncoder().encode(self)
        return try JSONSerialization.jsonObject(with: jsonData, options: .allowFragments)
    }
}

protocol DictionaryDecodable {
    static func decode(_ dictionary: Any) throws -> Self
}

extension DictionaryDecodable where Self: Decodable {
    static func decode(_ dictionary: Any) throws -> Self {
        let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [])
        return try JSONDecoder().decode(Self.self, from: jsonData)
    }
}

typealias DictionaryCodable = DictionaryEncodable & DictionaryDecodable

Et voici comment l'utiliser:

class AClass: Codable, DictionaryCodable {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

struct AStruct: Codable, DictionaryEncodable, DictionaryDecodable {
    
    var name: String
    var age: Int
}

let aClass = AClass(name: "Max", age: 24)

if let dict = try? aClass.encode(), let theClass = try? AClass.decode(dict) {
    print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theClass.name), age: \(theClass.age)\"")
}

let aStruct = AStruct(name: "George", age: 30)

if let dict = try? aStruct.encode(), let theStruct = try? AStruct.decode(dict) {
    print("Encoded dictionary: \n\(dict)\n\ndata from decoded dictionary: \"name: \(theStruct.name), age: \(theStruct.age)\"")
}
Spasbil
la source
0

Voici le dictionnaire -> objet. Swift 5.

extension Dictionary where Key == String, Value: Any {

    func object<T: Decodable>() -> T? {
        if let data = try? JSONSerialization.data(withJSONObject: self, options: []) {
            return try? JSONDecoder().decode(T.self, from: data)
        } else {
            return nil
        }
    }
}
Mike Glukhov
la source
-5

À bien y penser, la question n'a pas de réponse dans le cas général, car l' Encodableinstance peut être quelque chose de non sérialisable dans un dictionnaire, tel qu'un tableau:

let payload = [1, 2, 3]
let encoded = try JSONEncoder().encode(payload) // "[1,2,3]"

En dehors de cela, j'ai écrit quelque chose de similaire en tant que cadre .

zoul
la source
Je dois admettre que je ne comprends toujours pas pourquoi ce vote est défavorable :–) La mise en garde n'est-elle pas vraie? Ou le cadre n'est pas utile?
zoul