How to Use Swift'S Codable to Encode into a Dictionary

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

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

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)

Swift Codable Dictionary

Your data models are already defined correctly (however, I'd suggest some name changes and removing mutability/optionality from the properties).

Once you've parsed the JSON, there's no need to keep the Dictionary, since the keys are actually part of the value under the country-iso key.

So once you decoded your Root object, I would suggest simply keeping root.data.values, which gives you Array<CountryData>, which you can handle easily afterwards.

struct Root: Codable {
let data: [String: CountryData]
}

struct CountryData: Codable {
let countryID: Int
let countryISO, countryEng, countryFra: String
let datePublished: DatePublished

enum CodingKeys: String, CodingKey {
case countryID = "country-id"
case countryISO = "country-iso"
case countryEng = "country-eng"
case countryFra = "country-fra"
case datePublished = "date-published"
}
}

// MARK: - DatePublished
struct DatePublished: Codable {
let timestamp: Int
let date, asp: String
}

do {
let root = try JSONDecoder().decode(Root.self, from: countryJson.data(using: .utf8)!)
let countries = root.data.values
print(countries)
} catch {
error
}

Decode/encode dictionary keyed by Date

CodingKeyRepresentable was released in Swift 5.6, which now allows decoding/encoding Date keyed dictionaries using Codable.

For more info, see SE-0320.

Swift 5.6 was released on 14th March 2022. When using Xcode, you need at least Xcode 13.3 to use Swift 5.6.

How to encode/decode a dictionary with Codable values for storage in UserDefaults?

You are mixing up NSCoding and Codable. The former requires a subclass of NSObject, the latter can encode the structs and classes directly with JSONEncoder or ProperListEncoder without any Keyedarchiver which also belongs to NSCoding.

Your struct can be reduced to

struct Company: Codable {
var name : String
var initials : String
var logoURL : URL?
var brandColor : String?
}

That's all, the CodingKeys and the other methods are synthesized. I would at least declare name and initials as non-optional.

To read and save the data is pretty straightforward. The corresponding CompanyDefaults struct is

struct CompanyDefaults {

static private let companiesKey = "companiesKey"

static var companies: [String:Company] = {

guard let data = UserDefaults.standard.data(forKey: companiesKey) else { return [:] }
return try? JSONDecoder.decode([String:Company].self, from: data) ?? [:]
}() {
didSet {
guard let data = try? JSONEncoder().encode(companies) else { return }
UserDefaults.standard.set(data, forKey: companiesKey)
}
}
}

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


Related Topics



Leave a reply



Submit