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
Difference Between 'Let' and 'Var' in Swift
How to Use String.Substringwithrange? (Or, How Do Ranges Work in Swift)
Dyld: Library Not Loaded: @Rpath/Libswift_Stdlib_Core.Dylib
Xcode Swift Am/Pm Time to 24 Hour Format
Call a Method from a String in Swift
Transparent Background For Modally Presented Viewcontroller
Swift Alamofire: How to Get the Http Response Status Code
Constant Unassigned Optional Will Not Be Nil by Default
How to Get Mathemical Pi Constant in Swift
How to Multiply Two Arrays Element-Wise
Swift - Sort Array of Objects With Multiple Criteria
How to Dispatch_Sync, Dispatch_Async, Dispatch_After, etc in Swift 3, Swift 4, and Beyond
Getting Keyboard Size from Userinfo in Swift
How to Convert a View (Not Uiview) to an Image
Get a Swift Variable'S Actual Name as String