Encode Struct and Convert to Dictionary [String: Any]

Swift converting Decodable struct to Dictionary

You don't need it to be Decodable. What you need is to be able to encode it (Encodable). So start by declaring your structure as Codable. After encoding it you can convert your data into a dictionary using JSONSerialization jsonObject method:

extension Encodable {
func data(using encoder: JSONEncoder = .init()) throws -> Data { try encoder.encode(self) }
func string(using encoder: JSONEncoder = .init()) throws -> String { try data(using: encoder).string! }
func dictionary(using encoder: JSONEncoder = .init(), options: JSONSerialization.ReadingOptions = []) throws -> [String: Any] {
try JSONSerialization.jsonObject(with: try data(using: encoder), options: options) as? [String: Any] ?? [:]
}
}


extension Data {
func decodedObject<D: Decodable>(using decoder: JSONDecoder = .init()) throws -> D {
try decoder.decode(D.self, from: self)
}
}


extension Sequence where Element == UInt8 {
var string: String? { String(bytes: self, encoding: .utf8) }
}

I would also declare the srtuct properties as constants. If you need to change any value just create a new object:

struct DiscussionMessage: Codable {
let message, userCountryCode, userCountryEmoji, userName, userEmailAddress: String
let messageTimestamp: Double
let fcmToken, question, recordingUrl: String?
}


let message: DiscussionMessage = .init(message: "message", userCountryCode: "BRA", userCountryEmoji: "quot;, userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: "fcmToken", question: "question", recordingUrl: nil)

do {
let string = try message.string()
print(string) // {"fcmToken":"fcmToken","userName":"userName","message":"message","userCountryEmoji":"quot;,"userEmailAddress":"email@address.com","question":"question","messageTimestamp":1610557474.2272739,"userCountryCode":"BRA"}

let dictionary = try message.dictionary()
print(dictionary) // ["userName": userName, "userEmailAddress": email@address.com, "userCountryEmoji": , "messageTimestamp": 1610557474.227274, "question": question, "message": message, "fcmToken": fcmToken, "userCountryCode": BRA]

let data = try message.data() // 218 bytes
let decodedMessages: DiscussionMessage = try data.decodedObject()
print("decodedMessages", decodedMessages) // ecodedMessages DiscussionMessage(message: "message", userCountryCode: "BRA", userCountryEmoji: "quot;, userName: "userName", userEmailAddress: "email@address.com", messageTimestamp: 1610557474.227274, fcmToken: Optional("fcmToken"), question: Optional("question"), recordingUrl: nil)
} catch {
print(error)
}

Can Swift convert a class / struct data into dictionary?

You can just add a computed property to your struct to return a Dictionary with your values. Note that Swift native dictionary type doesn't have any method called value(forKey:). You would need to cast your Dictionary to NSDictionary:

struct Test {
let name: String
let age: Int
let height: Double
var dictionary: [String: Any] {
return ["name": name,
"age": age,
"height": height]
}
var nsDictionary: NSDictionary {
return dictionary as NSDictionary
}
}

You can also extend Encodable protocol as suggested at the linked answer posted by @ColGraff to make it universal to all Encodable structs:

struct JSON {
static let encoder = JSONEncoder()
}
extension Encodable {
subscript(key: String) -> Any? {
return dictionary[key]
}
var dictionary: [String: Any] {
return (try? JSONSerialization.jsonObject(with: JSON.encoder.encode(self))) as? [String: Any] ?? [:]
}
}

struct Test: Codable {
let name: String
let age: Int
let height: Double
}

let test = Test(name: "Alex", age: 30, height: 170)
test["name"] // Alex
test["age"] // 30
test["height"] // 170

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

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

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

Swift Decode [String: Any]

Decoders convert Data into Decodable values. They don't have anything to do with [String: Any] types or any other non-Data type. So if you want to run it through a decoder, you need to convert it to JSON encoded into Data.

If the [String: Any] results are exclusively JSONSerialization-safe types (arrays, dictionaries, strings, numbers, null), then JSONSerialization.data(withJSONObject:options:) would let you get back to data, so you can re-decode it. Your code doesn't just re-encode its results, it first turns it into an array:

let values = results.compactMap { $0.value }
let data = JSONSerialization.data (withJSONObject: values, options: [])

That's very strange. Do you really mean to create an array here and throw away the keys? I would then expect your JSONDecoder().decode() line to decode [T].self rather than T.self. So I would expect the following code (provided that your [String: Any] is JSON-safe):

public func call<T: Decodable>(completion handler: @escaping (T?) -> ()) {
let promise = api.getPromise()
promise.done(on: .main) { (results: [String:Any]) in
guard JSONSerialization.isValidJSONObject(results) else {
handler(nil)
return
}

let data = JSONSerialization.data(withJSONObject: results)
let decodedResults = try? JSONDecoder().decode(T.self, from: data)
handler(decodedResults)
}
}

In the comments you note that the decoded data (the [String: Any]) is not made of primitives. In that case it's not possible to re-encode it with JSONSerialization. You'll need to pass the [String: Any] to something that knows how to deal with it. For example:

protocol DictionaryDecodable {
init?(dictionary: [String: Any])
}

public func call<T: DictionaryDecodable>(completion handler: @escaping (T?) -> ()) {
let promise = api.getPromise ()
promise.done(on: .main) { (results: [String:Any])
handler(T.init(dictionary: results))
}
}

Your types will need to implement an init?(dictionary:) that can decode their own values out of a [String: Any].



Related Topics



Leave a reply



Submit