Bagaimana saya bisa menggunakan Swift's Codable untuk dikodekan ke dalam kamus?

110

Saya memiliki struct yang mengimplementasikan Swift 4 Codable. Apakah ada cara bawaan sederhana untuk menyandikan struct itu ke dalam kamus?

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

Jawaban:

231

Jika Anda tidak keberatan dengan sedikit pergeseran data di sekitar Anda, Anda dapat menggunakan sesuatu seperti ini:

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

Atau varian opsional

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

Dengan asumsi Foosesuai dengan Codableatau benar-benar Encodablemaka Anda dapat melakukan ini.

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

Jika Anda ingin pergi ke arah lain ( init(any)), lihat objek Init ini yang sesuai dengan Codable dengan kamus / larik

Chris Mitchelmore
sumber
22

Berikut adalah implementasi sederhana dari DictionaryEncoder/ DictionaryDecoderthat wrap JSONEncoder, JSONDecoderdan JSONSerialization, yang juga menangani strategi encoding / decoding…

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

Penggunaannya mirip dengan JSONEncoder/ JSONDecoder

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

dan

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

Untuk kenyamanan, saya telah menempatkan ini semua dalam repo…  https://github.com/ashleymills/SwiftDictionaryCoding

Ashley Mills
sumber
Terima kasih banyak!, Alternatifnya adalah menggunakan pewarisan tetapi situs pemanggil tidak dapat menyimpulkan jenisnya sebagai kamus karena akan ada 2 fungsi dari jenis pengembalian yang berbeda.
pengguna1046037
17

Saya telah membuat pustaka bernama CodableFirebase dan tujuan awalnya adalah menggunakannya dengan Firebase Database, tetapi sebenarnya ini melakukan apa yang Anda butuhkan: membuat kamus atau jenis lain seperti di JSONDecodertetapi Anda tidak perlu melakukan konversi ganda di sini seperti yang Anda lakukan di jawaban lain. Jadi akan terlihat seperti ini:

import CodableFirebase

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

Saya tidak yakin apakah itu cara terbaik tetapi Anda pasti dapat melakukan sesuatu seperti:

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
sumber
8
Ini hanya akan bekerja untuk struktur dengan semua properti dari jenis yang sama
Leo Dabus
1
Saya baru saja mencoba "let dict = try JSONDecoder (). Decode ([String: Int] .self, from: JSONEncoder (). Encode (foo))" dan saya mendapat "Diharapkan untuk memecahkan kode Dictionary <String, Any> tetapi menemukan array sebagai gantinya. " bisakah kamu membantu
Itan Hant
6

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

Ryan Collins
sumber
6

Tidak ada cara bawaan untuk melakukan itu. Seperti yang dijawab di atas, jika Anda tidak memiliki masalah kinerja, maka Anda dapat menerima JSONEncoder+ JSONSerializationimplementasi.

Tapi saya lebih suka menggunakan cara perpustakaan standar untuk menyediakan objek encoder / decoder.

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

Anda dapat mencobanya dengan kode berikut:

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)

Di sini saya mencoba paksa untuk mempersingkat contoh. Dalam kode produksi, Anda harus menangani kesalahan dengan tepat.

5keeve
sumber
4

Dalam beberapa proyek, saya menggunakan refleksi cepat. Tapi hati-hati, objek codable bersarang, jangan dipetakan juga di sana.

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

Saya benar-benar berpikir bahwa ada beberapa nilai dalam hanya dapat digunakan Codableuntuk menyandikan ke / dari kamus, tanpa niat untuk memukul JSON / Plists / apa pun. Ada banyak API yang hanya mengembalikan kamus, atau mengharapkan kamus, dan senang dapat menukarnya dengan mudah dengan struct atau objek Swift, tanpa harus menulis kode boilerplate tanpa akhir.

Saya telah bermain-main dengan beberapa kode berdasarkan sumber Foundation JSONEncoder.swift (yang sebenarnya mengimplementasikan encoding / decoding kamus secara internal, tetapi tidak mengekspornya).

Kode tersebut dapat ditemukan di sini: https://github.com/elegantchaos/DictionaryCoding

Ini masih cukup kasar, tetapi saya telah mengembangkannya sedikit sehingga, misalnya, dapat mengisi nilai yang hilang dengan default saat mendekode.

Sam Deane
sumber
2

Saya telah memodifikasi PropertyListEncoder dari proyek Swift menjadi DictionaryEncoder, cukup dengan menghapus serialisasi terakhir dari kamus ke dalam format biner. Anda dapat melakukan hal yang sama sendiri, atau Anda dapat mengambil kode saya dari sini

Ini bisa digunakan seperti ini:

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

Saya menulis inti singkat untuk menangani ini (tidak menggunakan protokol Codable). Hati-hati, ini tidak memeriksa ketikan nilai apa pun dan tidak berfungsi secara rekursif pada nilai yang dapat dienkode.

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

Tidak ada cara langsung untuk melakukan ini di Codable. Anda perlu menerapkan protokol Encodable / Decodable untuk struct Anda. Untuk contoh Anda, Anda mungkin perlu menulis seperti di bawah ini

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

Saya telah membuat pod di sini https://github.com/levantAJ/AnyCodable untuk memfasilitasi decode dan encode [String: Any] dan[Any]

pod 'DynamicCodable', '1.0'

Dan Anda dapat memecahkan kode & menyandikan [String: Any]dan[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
sumber
1
Contoh Anda tidak menunjukkan bagaimana menyelesaikan masalah
Simon Moshenko
0

Jika Anda menggunakan SwiftyJSON , Anda dapat melakukan sesuatu seperti ini:

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

Catatan: Anda juga dapat meneruskan kamus ini parametersuntuk permintaan Alamofire .

Jakub Truhlář
sumber
0

Berikut adalah solusi berbasis protokol:

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

Dan inilah cara menggunakannya:

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

Ini kamus -> objek. Cepat 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
sumber
-5

Kalau dipikir-pikir, pertanyaannya tidak memiliki jawaban dalam kasus umum, karena Encodableinstance tersebut mungkin sesuatu yang tidak dapat diserialkan ke dalam kamus, seperti array:

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

Selain itu, saya telah menulis sesuatu yang mirip sebagai kerangka kerja .

zoul
sumber
Saya harus mengakui bahwa saya masih tidak mengerti mengapa ini diturunkan suara :–) Apakah peringatan tersebut tidak benar? Atau kerangka tidak berguna?
zoul