Wie kann ich Swift's Codable verwenden, um in ein Wörterbuch zu codieren?

109

Ich habe eine Struktur, die Swift 4 implementiert Codable. Gibt es eine einfache integrierte Möglichkeit, diese Struktur in ein Wörterbuch zu kodieren?

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

Antworten:

230

Wenn es Ihnen nichts ausmacht, Daten ein wenig zu verschieben, können Sie Folgendes verwenden:

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

Oder eine optionale Variante

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

Vorausgesetzt, es Fooentspricht Codableoder wirklich, Encodabledann können Sie dies tun.

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

Wenn Sie in die andere Richtung gehen möchten ( init(any)), sehen Sie sich dieses Init an, ein Objekt, das Codable mit einem Wörterbuch / Array entspricht

Chris Mitchelmore
quelle
22

Hier sind einfache Implementierungen DictionaryEncoder/ DictionaryDecoderdass wickeln JSONEncoder, JSONDecoderund JSONSerialization, dass Griff auch Codierung / Decodierung Strategien ...

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

Die Verwendung ist ähnlich wie JSONEncoder/ JSONDecoder

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

und

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

Der Einfachheit halber habe ich dies alles in ein Repo eingefügt ...  https://github.com/ashleymills/SwiftDictionaryCoding

Ashley Mills
quelle
Vielen Dank! Die Alternative wäre die Verwendung der Vererbung, aber die aufrufende Site könnte den Typ nicht als Wörterbuch ableiten, da es zwei Funktionen mit unterschiedlichen Rückgabetypen geben würde.
user1046037
17

Ich habe eine Bibliothek namens CodableFirebase erstellt , deren ursprünglicher Zweck darin bestand, sie mit der Firebase-Datenbank zu verwenden, aber sie macht tatsächlich das, was Sie benötigen: Sie erstellt ein Wörterbuch oder einen anderen Typ wie in, JSONDecoderaber Sie müssen hier keine doppelte Konvertierung durchführen wie du es in anderen Antworten tust. Es würde also ungefähr so ​​aussehen:

import CodableFirebase

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

Ich bin mir nicht sicher, ob es der beste Weg ist, aber Sie können definitiv etwas tun wie:

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
quelle
8
Dies würde nur für Strukturen mit allen Eigenschaften der gleichen Art
funktionieren
1
Ich habe gerade versucht "let dict = try JSONDecoder (). Decode ([String: Int] .self, from: JSONEncoder (). Encode (foo))" und habe "Expected to Dode <String, Any>" gefunden, aber ein gefunden Array stattdessen. " Könnten Sie
bitte
6

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

Ryan Collins
quelle
6

Dafür gibt es keinen eingebauten Weg. Wie oben beantwortet , können Sie die JSONEncoder+ JSONSerialization-Implementierung akzeptieren, wenn Sie keine Leistungsprobleme haben .

Aber ich würde lieber den Weg der Standardbibliothek gehen, um ein Encoder / Decoder-Objekt bereitzustellen.

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

Sie können es mit folgendem Code versuchen:

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)

Ich versuche hier, das Beispiel zu verkürzen. Im Produktionscode sollten Sie die Fehler angemessen behandeln.

5keeve
quelle
4

In einem Projekt habe ich die schnelle Reflexion verwendet. Aber seien Sie vorsichtig, verschachtelte codierbare Objekte werden auch dort nicht zugeordnet.

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

Ich denke definitiv, dass es einen gewissen Wert hat, nur in der Lage zu sein Codable, in / aus Wörterbüchern zu codieren, ohne die Absicht, jemals JSON / Plists / was auch immer zu treffen. Es gibt viele APIs, die Ihnen nur ein Wörterbuch zurückgeben oder ein Wörterbuch erwarten, und es ist schön, sie einfach mit Swift-Strukturen oder -Objekten austauschen zu können, ohne endlosen Boilerplate-Code schreiben zu müssen.

Ich habe mit Code herumgespielt, der auf der Foundation JSONEncoder.swift-Quelle basiert (die die Wörterbuchcodierung / -decodierung zwar intern implementiert, aber nicht exportiert).

Den Code finden Sie hier: https://github.com/elegantchaos/DictionaryCoding

Es ist immer noch ziemlich rau, aber ich habe es ein wenig erweitert, damit beispielsweise fehlende Werte beim Dekodieren mit Standardwerten gefüllt werden können.

Sam Deane
quelle
2

Ich habe den PropertyListEncoder aus dem Swift-Projekt in einen DictionaryEncoder geändert , indem ich einfach die endgültige Serialisierung aus dem Wörterbuch in das Binärformat entfernt habe. Sie können das Gleiche selbst tun oder meinen Code von hier aus übernehmen

Es kann folgendermaßen verwendet werden:

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

Ich habe einen kurzen Überblick darüber gegeben (ohne das Codable-Protokoll). Seien Sie vorsichtig, es überprüft keine Werte und funktioniert nicht rekursiv für codierbare Werte.

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
quelle
0

In Codable gibt es keine direkte Möglichkeit, dies zu tun. Sie müssen das Encodable / Decodable-Protokoll für Ihre Struktur implementieren. Für Ihr Beispiel müssen Sie möglicherweise wie folgt schreiben

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
quelle
0

Ich habe hier einen Pod erstellt https://github.com/levantAJ/AnyCodable , um das Dekodieren und Kodieren zu erleichtern [String: Any]und[Any]

pod 'DynamicCodable', '1.0'

Und Sie können dekodieren und kodieren [String: Any]und[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
quelle
1
Ihr Beispiel zeigt nicht, wie das Problem gelöst werden kann
Simon Moshenko
0

Wenn Sie SwiftyJSON verwenden , können Sie Folgendes tun:

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

Hinweis: Sie können dieses Wörterbuch auch parametersfür Alamofire- Anforderungen übergeben.

Jakub Truhlář
quelle
0

Hier ist eine protokollbasierte Lösung:

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

Und so geht's:

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
quelle
0

Hier ist Wörterbuch -> Objekt. 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
quelle
-5

Wenn Sie sich das vorstellen, hat die Frage im allgemeinen Fall keine Antwort, da die EncodableInstanz möglicherweise nicht in ein Wörterbuch serialisierbar ist, z. B. ein Array:

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

Davon abgesehen habe ich etwas Ähnliches als Framework geschrieben .

Zoul
quelle
Ich muss zugeben, dass ich immer noch nicht verstehe, warum dies abgelehnt wird :–) Stimmt die Einschränkung nicht? Oder das Framework nicht sinnvoll?
Zoul