Jak mogę używać Swift's Codable do kodowania w słowniku?

110

Mam strukturę, która implementuje Swift 4 Codable. Czy istnieje prosty wbudowany sposób zakodowania tej struktury w słowniku?

let struct = Foo(a: 1, b: 2)
let dict = something(struct)
// now dict is ["a": 1, "b": 2]
zoul
źródło

Odpowiedzi:

232

Jeśli nie przeszkadza ci trochę przesunięcie danych, możesz użyć czegoś takiego:

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

Lub opcjonalny wariant

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

Zakładając, że się Foozgadza Codablelub naprawdę, Encodablemożesz to zrobić.

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

Jeśli chcesz iść w drugą stronę ( init(any)), spójrz na to Inituj obiekt zgodny z Codable ze słownikiem / tablicą

Chris Mitchelmore
źródło
Opcjonalna implementacja var jest świetna, przejrzysta, szybka i idealna do wypowiedzi guard let. Naprawdę czyści wywołania API.
Iron John Bonney
22

Oto proste implementacje DictionaryEncoder/ DictionaryDecoderktóre owijają JSONEncoder, JSONDecoderoraz JSONSerialization, że również obsługiwać kodowanie / dekodowanie strategie ...

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

Użycie jest podobne do JSONEncoder/ JSONDecoder

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

i

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

Dla wygody umieściłem to wszystko w repozytorium…  https://github.com/ashleymills/SwiftDictionaryCoding

Ashley Mills
źródło
Wielkie dzięki !, alternatywą byłoby użycie dziedziczenia, ale strona wywołująca nie byłaby w stanie wywnioskować typu jako słownika, ponieważ byłyby 2 funkcje o różnych typach zwracanych.
user1046037
17

Utworzyłem bibliotekę o nazwie CodableFirebase i początkowym celem było użycie jej z bazą danych Firebase, ale faktycznie robi to, czego potrzebujesz: tworzy słownik lub dowolny inny typ, tak jak w, JSONDecoderale nie musisz tutaj wykonywać podwójnej konwersji tak jak w innych odpowiedziach. Więc wyglądałoby to mniej więcej tak:

import CodableFirebase

let model = Foo(a: 1, b: 2)
let dict = try! FirebaseEncoder().encode(model)
Noobass
źródło
7

Nie jestem pewien, czy to najlepszy sposób, ale na pewno możesz zrobić coś takiego:

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
źródło
8
To zadziałałoby tylko dla struktur o wszystkich właściwościach tego samego rodzaju
Leo Dabus
1
Właśnie spróbowałem „let dict = try JSONDecoder (). Decode ([String: Int] .self, from: JSONEncoder (). Encode (foo))” i otrzymałem „Oczekiwano dekodowania Dictionary <String, Any>, ale znalazłem zamiast tablicy. " czy mógłbyś pomóc,
proszę
6

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

Ryan Collins
źródło
6

Nie ma na to sposobu. Zgodnie z powyższą odpowiedzią, jeśli nie masz problemów z wydajnością, możesz zaakceptować plikJSONEncoder + JSONSerializationwdrożenie.

Ale wolałbym raczej użyć standardowej biblioteki, aby zapewnić obiekt kodera / dekodera.

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

Możesz spróbować z następującym kodem:

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)

Staram się tutaj na siłę skrócić przykład. W kodzie produkcyjnym należy odpowiednio obsługiwać błędy.

5keeve
źródło
4

W niektórych projektach korzystam z szybkiej refleksji. Ale uważaj, zagnieżdżone obiekty kodowalne również nie są tam mapowane.

let dict = Dictionary(uniqueKeysWithValues: Mirror(reflecting: foo).children.map{ ($0.label!, $0.value) })
Nikolay Kapustin
źródło
2

Zdecydowanie uważam, że po prostu możliwość używania ma jakąś wartość Codable kodowania do / ze słowników, bez zamiaru kiedykolwiek uderzania w JSON / Plists / cokolwiek. Istnieje wiele interfejsów API, które po prostu zwracają słownik lub oczekują słownika, i miło jest móc łatwo wymieniać je ze strukturami lub obiektami Swift, bez konieczności pisania niekończącego się standardowego kodu.

Bawiłem się kodem opartym na źródle Foundation JSONEncoder.swift (które faktycznie implementuje wewnętrzne kodowanie / dekodowanie słownika, ale go nie eksportuje).

Kod można znaleźć tutaj: https://github.com/elegantchaos/DictionaryCoding

Nadal jest dość szorstki, ale nieco go rozszerzyłem, aby na przykład mógł uzupełniać brakujące wartości domyślnymi wartościami podczas dekodowania.

Sam Deane
źródło
2

Zmodyfikowałem PropertyListEncoder z projektu Swift na DictionaryEncoder, po prostu usuwając końcową serializację ze słownika do formatu binarnego. Możesz zrobić to samo samodzielnie lub pobrać mój kod stąd

Można go używać w następujący sposób:

do {
    let employeeDictionary: [String: Any] = try DictionaryEncoder().encode(employee)
} catch let error {
    // handle error
}
Marmoy
źródło
0

Napisałem krótkie streszczenie, jak sobie z tym poradzić (nie używając protokołu Codable). Uważaj, nie sprawdza typu żadnych wartości i nie działa rekurencyjnie na wartościach, które można zakodować.

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
źródło
0

W Codable nie ma na to prostego sposobu. Musisz zaimplementować protokół Encodable / Decodable dla swojej struktury. Na przykład możesz potrzebować napisać jak poniżej

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
źródło
0

Zrobiłem tutaj pod https://github.com/levantAJ/AnyCodable, aby ułatwić dekodowanie i kodowanie [String: Any] oraz[Any]

pod 'DynamicCodable', '1.0'

Jesteś w stanie dekodować i kodować [String: Any]oraz[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
źródło
1
Twój przykład nie pokazuje, jak rozwiązać problem
Simon Moshenko
0

Jeśli używasz SwiftyJSON , możesz zrobić coś takiego:

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

Uwaga: Można także przekazać ten słownik jako parametersdo Alamofire żądań.

Jakub Truhlář
źródło
0

Oto rozwiązanie oparte na protokole:

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

A oto jak z niego korzystać:

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
źródło
0

Oto słownik -> obiekt. Szybki 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
źródło
-5

Jeśli się nad tym zastanowić, to pytanie nie ma odpowiedzi w ogólnym przypadku, ponieważ Encodableinstancja może być czymś, czego nie można serializować w słowniku, na przykład tablicą:

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

Poza tym napisałem coś podobnego jako framework .

zoul
źródło
Muszę przyznać, że nadal nie rozumiem, dlaczego jest to odrzucane :–) Czy zastrzeżenie nie jest prawdziwe? Czy ramy nie są przydatne?
zoul