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)
}
}
}
Swift 4 Decodable - Dictionary with enum as key
The problem is that Dictionary
's Codable
conformance can currently only properly handle String
and Int
keys. For a dictionary with any other Key
type (where that Key
is Encodable
/Decodable
), it is encoded and decoded with an unkeyed container (JSON array) with alternating key values.
Therefore when attempting to decode the JSON:
{"dictionary": {"enumValue": "someString"}}
into AStruct
, the value for the "dictionary"
key is expected to be an array.
So,
let jsonDict = ["dictionary": ["enumValue", "someString"]]
would work, yielding the JSON:
{"dictionary": ["enumValue", "someString"]}
which would then be decoded into:
AStruct(dictionary: [AnEnum.enumValue: "someString"])
However, really I think that Dictionary
's Codable
conformance should be able to properly deal with any CodingKey
conforming type as its Key
(which AnEnum
can be) – as it can just encode and decode into a keyed container with that key (feel free to file a bug requesting for this).
Until implemented (if at all), we could always build a wrapper type to do this:
struct CodableDictionary<Key : Hashable, Value : Codable> : Codable where Key : CodingKey {
let decoded: [Key: Value]
init(_ decoded: [Key: Value]) {
self.decoded = decoded
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
decoded = Dictionary(uniqueKeysWithValues:
try container.allKeys.lazy.map {
(key: $0, value: try container.decode(Value.self, forKey: $0))
}
)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
for (key, value) in decoded {
try container.encode(value, forKey: key)
}
}
}
and then implement like so:
enum AnEnum : String, CodingKey {
case enumValue
}
struct AStruct: Codable {
let dictionary: [AnEnum: String]
private enum CodingKeys : CodingKey {
case dictionary
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dictionary = try container.decode(CodableDictionary.self, forKey: .dictionary).decoded
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(CodableDictionary(dictionary), forKey: .dictionary)
}
}
(or just have the dictionary
property of type CodableDictionary<AnEnum, String>
and use the auto-generated Codable
conformance – then just speak in terms of dictionary.decoded
)
Now we can decode the nested JSON object as expected:
let data = """
{"dictionary": {"enumValue": "someString"}}
""".data(using: .utf8)!
let decoder = JSONDecoder()
do {
let result = try decoder.decode(AStruct.self, from: data)
print(result)
} catch {
print(error)
}
// AStruct(dictionary: [AnEnum.enumValue: "someString"])
Although that all being said, it could be argued that all you're achieving with a dictionary with an enum
as a key is just a struct
with optional properties (and if you expect a given value to always be there; make it non-optional).
Therefore you may just want your model to look like:
struct BStruct : Codable {
var enumValue: String?
}
struct AStruct: Codable {
private enum CodingKeys : String, CodingKey {
case bStruct = "dictionary"
}
let bStruct: BStruct
}
Which would work just fine with your current JSON:
let data = """
{"dictionary": {"enumValue": "someString"}}
""".data(using: .utf8)!
let decoder = JSONDecoder()
do {
let result = try decoder.decode(AStruct.self, from: data)
print(result)
} catch {
print(error)
}
// AStruct(bStruct: BStruct(enumValue: Optional("someString")))
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)
}
}
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"))
}
}
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
}
How to extend Codable functionality of Date and other built in types?
I finally figured out a way to do this with the following code:
fileprivate struct DateWrapper: Decodable {
var date: Date
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
date = try container.decode(Date.self)
}
}
extension KeyedDecodingContainer {
private enum TimeCodingKeys: String, CodingKey {
case datetime
}
func decode(_ type: Date.Type, forKey key: K) throws -> Date {
let dateString: String
if let timeContainer = try? self.nestedContainer(keyedBy: TimeCodingKeys.self, forKey: key) {
dateString = try timeContainer.decode(String.self, forKey: .datetime)
} else if let string = try? self.decode(String.self, forKey: key) {
dateString = string
} else {
return try self.decode(DateWrapper.self, forKey: key).date
}
if let date = Utils.date(from: dateString) {
return date
} else if let date = Utils.date(from: dateString, with: "yyyy-MM-dd") {
return date
} else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Date format was not parseable.")
throw DecodingError.dataCorrupted(context)
}
}
}
The issue with trying to recreate the code of Date.init(from:Decoder)
is that the type information is also encoded in the plist entry, so even though I knew the entry for the date was encoded as a Double
, it would not let me extract a Double
because that's not what the type tag says. I also could not call the default implementation of decode(Date.self, forKey: key)
because that's the function I'm writing and this isn't a subclass so I can't call super
. I tried a few clever things trying to extract the concrete Decoder
from the KeyedDecodingContainer
so I could call Date.init(from:Decoder)
directly, but that didn't work because the context of the particular key was lost when I got the Decoder
back. (See https://stablekernel.com/understanding-extending-swift-4-codable/ if you're curious about extracting Decoder
s).
I knew I could achieve what I wanted by using a wrapper around Date
to do the weird decoding, but I didn't want to have to append .date
to all the places where I use dates in my codebase. Then I realized that for this default case that I was stuck on, the wrapper would allow me to extract the date from a SingleValueDecodingContainer
instead of from a KeyedDecodingContainer
, allowing me to call the default Date
decoding code without ending up in an infinite loop calling my custom function.
This is probably super jank and inappropriate, but it works, and will save me a lot of boilerplate until I can get my API standardized.
EDIT: I rearranged this a bit to have better division of responsibilities and made it work with more container types
fileprivate struct DateWrapper: Decodable {
var date: Date
private enum TimeCodingKeys: String, CodingKey {
case datetime
}
init(from decoder: Decoder) throws {
let dateString: String
if let timeContainer = try? decoder.container(keyedBy: TimeCodingKeys.self) {
dateString = try timeContainer.decode(String.self, forKey: .datetime)
} else {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
dateString = string
} else {
date = try container.decode(Date.self)
return
}
}
if let date = Utils.date(from: dateString) {
self.date = date
} else if let date = Utils.date(from: dateString, with: "yyyy-MM-dd") {
self.date = date
} else {
let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Date format was not parseable.")
throw DecodingError.dataCorrupted(context)
}
}
}
extension KeyedDecodingContainer {
func decode(_ type: Date.Type, forKey key: K) throws -> Date {
return try self.decode(DateWrapper.self, forKey: key).date
}
func decode(_ type: [Date].Type, forKey key: K) throws -> [Date] {
var container = try nestedUnkeyedContainer(forKey: key)
var dates: [Date] = []
while !container.isAtEnd {
dates.append(try container.decode(Date.self))
}
return dates
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Date.Type) throws -> Date {
return try self.decode(DateWrapper.self).date
}
}
Changing key type on JSON dictionary
You cannot decode JSON keys with a decoding strategy.
The following code can be certainly optimized but this maps the string keys to Date
struct Timeline: Codable {
let formatter : DateFormatter = {
let fm = DateFormatter()
fm.locale = Locale(identifier: "en_US_POSIX")
fm.dateFormat = "M/dd/yy"
return fm
}()
private enum CodingKeys : String, CodingKey { case cases, deaths, recovered }
let cases, deaths, recovered: [Date: Int]
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let casesData = try container.decode([String: Int].self, forKey: .cases)
var caseResult = [Date:Int]()
for (key, value) in casesData {
let date = formatter.date(from: key)!
caseResult[date] = value
}
cases = caseResult
let deathsData = try container.decode([String: Int].self, forKey: .deaths)
var deathsResult = [Date:Int]()
for (key, value) in deathsData {
let date = formatter.date(from: key)!
deathsResult[date] = value
}
deaths = deathsResult
let recoveredData = try container.decode([String: Int].self, forKey: .recovered)
var recoveredResult = [Date:Int]()
for (key, value) in recoveredData {
let date = formatter.date(from: key)!
recoveredResult[date] = value
}
recovered = recoveredResult
}
}
Swift - Encode and Decode a dictionary [String:Any] into plist
So finally worked it out with the help of Andrada.
I added a second struct which held the action and by passed having to use [string:any]
class Marker : Encodable, Decodable {
var UUIDpic: UUID = UUID()
var alpha: Int = 1
var buttonType: Int = 0
var buttonAction : [String: [ButtonAction]] = [:] //Dictionary I edited using the new struct
var buttonNameColor: String = ""
var buttonNameFontSize: Int = 10
var buttonShape: String = ""
var loggerRect: String = ""
var maskColor: String = ""
var name: String = ""
}
Below is the struct I added
struct ButtonAction: Codable {
var action: String
var array_linked_of_buttons: [[String:String]]
init(action: String, array_linked_of_buttons: [[String:String]]) {
self.action = action
self.array_linked_of_buttons = array_linked_of_buttons
}
}
Make sure to init your struct or it won't work.
Related Topics
Tests for Custom Uitableviewcell, Cellforrowatindexpath Crashes with Nil Outlets
(Cross-)Compiling Swift for Raspberry Pi
Structuring Data for Chat App in Firebase
Swift Optional Variable Assignment with Default Value (Double Question Marks)
What Does Cloning a Github Repository Mean
Swiftui: Stop an Animation That Repeats Forever
Swift - Associated Value or Extension for an Enum
Differencebetween Convenience Init VS Init in Swift, Explicit Examples Better
Using the Swift If Let with Logical and Operator &&
What Does an Exclamation Mark in a Property in Swift Language
What Is the Shortest Way to Run Same Code N Times in Swift
How to Use a Switch Statement with a Nested Enum
Color Ouput with Swift Command Line Tool
Receiving Data from Nsinputstream in Swift
Animated Curve Line in Swift 3
"Ambiguous Use of 'Children'" When Trying to Use Nstreecontroller.Arrangedobjects in Swift 3.0