Convert Swift Encodable Class Typed as Any to Dictionary

Convert Swift Encodable class typed as Any to dictionary

The problem is that protocols don't always conform to themselves. PropertyListEncoder's encode(_:) method expects a Value : Encodable argument:

func encode<Value : Encodable>(_ value: Value) throws -> Data

However the Encodable type itself is currently unable to satisfy this constraint (but it might well do in a future version of the language).

As explored in the linked Q&A (and also here), one way to work around this limitation is to open the Encodable value in order to dig out the underlying concrete type, which we can substitute for Value. We can do this with a protocol extension, and use a wrapper type in order to encapsulate it:

extension Encodable {
fileprivate func openedEncode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}

struct AnyEncodable : Encodable {
var value: Encodable
init(_ value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.openedEncode(to: &container)
}
}

Applied to your example:

class NSPresetArrayController : NSArrayController {
override func addObject(_ object: Any) {
guard let object = object as? Encodable else {
// Not encodable, maybe do some error handling.
return
}
do {
let encoded = try PropertyListEncoder().encode(AnyEncodable(object))
let cocoaPropertyList = try PropertyListSerialization.propertyList(from: encoded, format: nil)
super.addObject(cocoaPropertyList)
} catch {
// Couldn't encode. Do some error handling.
}
}
}

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 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 to directly convert a Dictionary to a Codable instance in Swift?

It looks like you have JSON that looks like this:

let r1 = Data("""
{"code": "0", "info": {"user_id": 123456}}
""".utf8)

let r2 = Data("""
{"code": "0", "info": {"temperature": 20, "country": "London"}}
""".utf8)

And "info" types that look like this:

struct UserID: Codable {
var userId: Int
}

struct WeatherTemperature: Codable {
var temperature: Int
var country: String
}

I'm assuming a decoder that does the snakeCase conversion (you can replace this by implementing CodingKeys or whatever):

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

Given that, you need a generic Response type:

struct Response<Info: Codable>: Codable {
var code: String
var info: Info
}

And with that, you can decode your response directly from the JSON object:

let userID = try decoder.decode(Response<UserID>.self, from: r1).info
let weather = try decoder.decode(Response<WeatherTemperature>.self, from: r2).info

Encode dictionary without adding the coding key enum in Swift

As with almost all custom encoding problems, the tool you need is AnyStringKey (it frustrates me that this isn't in stdlib):

struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init(_ stringValue: String) { self.init(stringValue: stringValue) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}

This just lets you encode and encode arbitrary keys. With this, the encoder is straightforward:

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)
for (key, value) in content {
try container.encode(value, forKey: AnyStringKey(key))
}
try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}

This assumes you mean to allow multiple key/value pairs in Content. I expect you don't; you're just using a dictionary because you want a better way to encode. If Content has a single key, then you can rewrite it a bit more naturally this way:

// Content only encodes getTrouble; it doesn't encode key
struct Content:Codable{
let key: String
let getTrouble: Bool

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(["get_trouble": getTrouble])
}
}

struct Request: Codable {
// ...

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)
try container.encode(content, forKey: AnyStringKey(content.key))
try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}
}

Now that may still bother you because it pushes part of the Content encoding logic into Request. (OK, maybe it just bothers me.) If you put aside Codable for a moment, you can fix that too.

// Encode Content directly into container
extension KeyedEncodingContainer where K == AnyStringKey {
mutating func encode(_ value: Content) throws {
try encode(["get_trouble": value.getTrouble], forKey: AnyStringKey(value.key))
}
}

struct Request: Codable {
// ...

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)

// And encode into the container (note no "forKey")
try container.encode(content)

try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}
}

How to decode a property with type of JSON dictionary in Swift [45] decodable protocol

With some inspiration from this gist I found, I wrote some extensions for UnkeyedDecodingContainer and KeyedDecodingContainer. You can find a link to my gist here. By using this code you can now decode any Array<Any> or Dictionary<String, Any> with the familiar syntax:

let dictionary: [String: Any] = try container.decode([String: Any].self, forKey: key)

or

let array: [Any] = try container.decode([Any].self, forKey: key)

Edit: there is one caveat I have found which is decoding an array of dictionaries [[String: Any]] The required syntax is as follows. You'll likely want to throw an error instead of force casting:

let items: [[String: Any]] = try container.decode(Array<Any>.self, forKey: .items) as! [[String: Any]]

EDIT 2: If you simply want to convert an entire file to a dictionary, you are better off sticking with api from JSONSerialization as I have not figured out a way to extend JSONDecoder itself to directly decode a dictionary.

guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
// appropriate error handling
return
}

The extensions

// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a

struct JSONCodingKeys: CodingKey {
var stringValue: String

init?(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}

extension KeyedDecodingContainer {

func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}

func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
guard try decodeNil(forKey: key) == false else {
return nil
}
return try decode(type, forKey: key)
}

func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()

for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}

extension UnkeyedDecodingContainer {

mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
// See if the current value in the JSON array is `null` first and prevent infite recursion with nested arrays.
if try decodeNil() {
continue
} else if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}

mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}

How to encode a dictionary of Codables in Swift?

Codable, as a generic constraint, and Any are not encodable. Use a struct instead of a dictionary:

struct A: Codable {
let a = 0
}
struct B: Codable {
let b = "hi"
}
struct C: Codable {
let a: A
let b: B
}

let d = C(a: A(), b: B())
let data = try JSONEncoder().encode(d)


Related Topics



Leave a reply



Submit