zoul
zoul

Reputation: 104115

How can I use Swift’s Codable to encode into a dictionary?

I have a struct that implements Swift 4’s Codable. Is there a simple built-in way to encode that struct into a dictionary?

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

Upvotes: 174

Views: 148966

Answers (16)

Muhammad Ahmed Baig
Muhammad Ahmed Baig

Reputation: 726

Ashley Mill's Answer in POP way:

To Enable functionality in any Class / Struct.

protocol DictionaryEncoder {
    associatedtype T : Encodable
    func encode<T>(_ value: T) throws -> [String: Any]?
}
extension DictionaryEncoder {
    func encode(_ value: T) throws -> [String: Any]? {
        let data = try JSONEncoder().encode(value)
        return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
    }
}

protocol DictionaryDecoder {
    associatedtype T : Decodable
    func decode(_ type: T.Type, from dictionary: [String: Any]) throws -> T
}
extension DictionaryDecoder {
    func decode(_ type: T.Type, from dictionary: [String: Any]) throws -> T {
        let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
        return try JSONDecoder().decode(type, from: data)
    }
}

To Enable functionality directly in Encodable / Decodable Object.

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

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

Upvotes: 0

Chris Mitchelmore
Chris Mitchelmore

Reputation: 6186

If you don't mind a bit of shifting of data around you could use something like this:

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

Or an optional variant (syntax fix for 2024)

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

Assuming Foo conforms to Codable or really Encodable then you can do this.

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

If you want to go the other way(init(any)), take a look at this Init an object conforming to Codable with a dictionary/array

Upvotes: 365

5keeve
5keeve

Reputation: 186

There is no built in way to do that. As answered above if you have no performance issues then you can accept the JSONEncoder + JSONSerialization implementation.

But I would rather go the standard library's way to provide an encoder/decoder object.

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: .fragmentsAllowed)
    }
}

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: .fragmentsAllowed)
        return try jsonDecoder.decode(type, from: jsonData)
    }
}

You can try it with following code:

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)

I am force-trying here to make the example shorter. In production code you should handle the errors appropriately.

Upvotes: 12

Talha Rasool
Talha Rasool

Reputation: 1152

After research, we find that if we use the keyword Any in the class which is inherited from the Codable & Decodable it will give the error. So if you want to use a dictionary user with the types of data coming from the server. For example, the server is sending the dictionary of type [String : Int] then use [String : Int] if you will try [String : Any] it will not work.

Upvotes: 0

spasbil
spasbil

Reputation: 279

Here is a protocol based solution:

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

And here is how to use it:

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

Upvotes: 3

Tai Le
Tai Le

Reputation: 9296

I have made a pod here https://github.com/levantAJ/AnyCodable to facilitate decode and encode [String: Any] and [Any]

pod 'DynamicCodable', '1.0'

And you are able to decode & encode [String: Any] and [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)
    }
}

Upvotes: 0

Ashley Mills
Ashley Mills

Reputation: 53203

Here are simple implementations of DictionaryEncoder / DictionaryDecoder that wrap JSONEncoder, JSONDecoder and JSONSerialization, that also handle encoding / decoding strategies…

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

Usage is similar to JSONEncoder / JSONDecoder

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

and

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

For convenience, I've put this all in a repo… https://github.com/ashleymills/SwiftDictionaryCoding

Upvotes: 44

Nikolay Kapustin
Nikolay Kapustin

Reputation: 71

In some project, i'm used the swift reflection. But be careful, nested codable objects, are not mapped also there.

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

Upvotes: 6

Kraming
Kraming

Reputation: 257

There no straight forward way of doing this in Codable. You need to implement Encodable/Decodable protocol for your struct. For your example, you might need to write as below

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

Upvotes: 0

Marmoy
Marmoy

Reputation: 8079

I have modified the PropertyListEncoder from the Swift project into a DictionaryEncoder, simply by removing the final serialisation from dictionary into binary format. You can do the same yourself, or you can take my code from here

It can be used like this:

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

Upvotes: 5

Sam Deane
Sam Deane

Reputation: 1623

I definitely think that there's some value in just being able to use Codable to encode to/from dictionaries, without the intention of ever hitting JSON/Plists/whatever. There are plenty of APIs which just give you back a dictionary, or expect a dictionary, and it's nice to be able to interchange them easily with Swift structs or objects, without having to write endless boilerplate code.

I've been playing round with some code based on the Foundation JSONEncoder.swift source (which actually does implement dictionary encoding/decoding internally, but doesn't export it).

The code can be found here: https://github.com/elegantchaos/DictionaryCoding

It's still quite rough, but I've expanded it a bit so that, for example, it can fill in missing values with defaults when decoding.

Upvotes: 3

Noobass
Noobass

Reputation: 2014

I have create a library called CodableFirebase and it's initial purpose was to use it with Firebase Database, but it does actually what you need: it creates a dictionary or any other type just like in JSONDecoder but you don't need to do the double conversion here like you do in other answers. So it would look something like:

import CodableFirebase

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

Upvotes: 20

Ryan Collins
Ryan Collins

Reputation: 729

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

Upvotes: 8

Sid Mani
Sid Mani

Reputation: 369

I wrote a quick gist to handle this (not using the Codable protocol). Be careful, it doesn't type-check any values and doesn't work recursively on values that are encodable.

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

Upvotes: 1

zoul
zoul

Reputation: 104115

Come to think of it, the question does not have an answer in the general case, since the Encodable instance may be something not serializable into a dictionary, such as an array:

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

Other than that, I have written something similar as a framework.

Upvotes: -6

Lawliet
Lawliet

Reputation: 3499

I'm not sure if it's the best way but you definitely can do something like:

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)

Upvotes: 6

Related Questions