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
Change The 2Nd and 3Rd Pickerview Acording to What Row from The 1St Picker Is Selected
Cloudkit: How to Access Main User's Attributes
Why Run Loop Is Needed When Using Dispatchqueue.Main.Async in MAC Command Line Tool in Swift
Swiftui Sheet Shows Sheet with Wrong Data
Can't Unwrap Optional.None When Setting Window Background Color
Uirefreshcontrol Glitching in Combination with Custom Tableviewcell
What Is The Intended Use of Optional Variable/Constant in Swift
Why Do I Keep Getting The Error "No Such Module 'Realmswift'"
Swift-Making an Sknode Simply "Move Forward" on an Angle
Use Tableviewcontroller Inside Skscene
How to Count Number of Sprites Swift
Using UIscrollview Correctly in Swiftui
Can Not Import Tensorflow for Swift in Xcode Playground
Connect Physicsbodies on Tilemap in Spritekit
Extending Dictionary with Key and Value Constraints
Applescript Used in My Cocoa MAC App, Stopped Working in Osx 10.14