Decode/Encode Dictionary Keyed by Date

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 Decoders).

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



Leave a reply



Submit