Dictionary with Coding keys in Swift, Codale to String [duplicate] - swift

This question already has answers here:
How can I make a Decodable object from a dictionary?
(2 answers)
Closed 6 months ago.
I have valid, working code, but I want to find out if there is a way to make it simpler and smaller.
I have a custom class Response which can be initialised from Json or text (depends on response from server)
public class Response: Codable {
let responseP1: String?
let responseP2: String?
let responseP3: String?
enum CodingKeys: String, CodingKey {
case responseP1 = "someResponseCode1"
case responseP2 = "someResponseCode2"
case responseP3 = "someResponseCode3"
}
required init(_ response: [String: String]) {
self.responseP1 = response["someResponseCode1"]
self.responseP3 = response["someResponseCode2"]
self.responseP2 = response["someResponseCode3"]
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.responseP1 = try container.decode(String.self, forKey: .responseP2)
self.responseP3 = try container.decode(String.self, forKey: .responseP1)
self.responseP2 = try container.decode(String.self, forKey: .responseP3)
}
}
can I combine Coding keys and initialisation with Dictionary somehow?
e.g. self.responseP1 = response[.responseP1]
but self.responseP1 = response[CodingKeys.responseP1.rawValue] is working but looks like I am winning nothing in this case
Also I need to parse it all to String, but
public func encodeAsString() -> String? {
do {
let encodedResponse = try self.encoded()
return String(decoding: encodedResponse, as: UTF8.self)
} catch {
return nil
}
}
does not work for me (returns "{}" even when was initialised from Json not Text), can you give advise why?

If your goal is just to be able to write self.responseP1 = response[.responseP1], then you need to convert the Dictionary to the right type [CodingKeys: String].
private static func convertKeys(from response: [String: String]) -> [CodingKeys: String] {
Dictionary(uniqueKeysWithValues: response.compactMap {
guard let key = CodingKeys.init(stringValue: $0) else { return nil }
return (key: key, value: $1)
})
}
required init(_ response: [String: String]) {
let response = Self.convertKeys(from: response)
self.responseP1 = response[.responseP1]
self.responseP3 = response[.responseP2]
self.responseP2 = response[.responseP3]
}

Related

swift dictionaries: map within a map

I'd like to convert a value I get from an API to a specific format.
[String:Any] // format received
[Int:[ContentType:Int]] // required format
ContentType is an Enum
An example of the data might look like this:
["123":["Tables":"25","Chairs":"14"]] // input
[123:[.Tables:25,.Chairs:14]] // output
I think I need to have a map within a map for this to work, but I'm struggling to work out a way forward. I may well be barking up the wrong tree entirely though. I don't really want to manually loop through and add each item one at a time; I'm looking for something more intelligent than that if possible.
enum ContentType: String {
case Tables,Chairs
}
let original_values: [String:Any]
= ["1234":["Tables":"5","Chairs":"2"]]
let values: [Int:[ContentType:Int]]
= Dictionary(uniqueKeysWithValues: original_values.map {
(
Int($0.key)!,
(($0.value as? [String:String]).map { // Error on this line - expects 1 argument but two were used
(
ContentType(rawValue: $1.key)!, // $1 is presumably wrong here?
Int($1.value)
)
}) as? [ContentType:Int]
)
})
Any ideas anybody?
I'd like to convert a value I get from an API to a specific format.
You can make your enum Decodable
enum ContentType: String, Decodable {
case tables, chairs
enum CodingKeys: String, CodingKey {
case Tables = "Tables"
case Chairs = "Chairs"
}
}
Then you can decode received Data and then compactMap it to format (Int, [ContentType: Int]). These tuples you can convert to Dictionary using designed initializer
do {
let decoded = try JSONDecoder().decode([String: [ContentType: Int]].self, from: data)
let mapped = Dictionary(uniqueKeysWithValues: decoded.compactMap { (key,value) -> (Int, [ContentType: Int])? in
if let int = Int(key) {
return (int, value)
} else {
return nil
}
})
} catch {
print(error)
}
On this line:
(($0.value as? [String:String]).map {
You using not Sequence.map, but Optional.map.
Working solution:
/// First let's map plain types to our types
let resultArray = original_values
.compactMap { (key, value) -> (Int, [ContentType: Int])? in
guard let iKey = Int(key), let dValue = value as? [String: String] else { return nil }
let contentValue = dValue.compactMap { (key, value) -> (ContentType, Int)? in
guard let cKey = ContentType(rawValue: key), let iValue = Int(value) else { return nil }
return (cKey, iValue)
}
let contentDict = Dictionary(uniqueKeysWithValues: contentValue)
return (iKey, contentDict)
}
let result = Dictionary(uniqueKeysWithValues: resultArray)
To improve print output add conform to CustomStringConvertible:
extension ContentType: CustomStringConvertible {
var description: String {
switch self {
case .Tables:
return "Tables"
case .Chairs:
return "Chairs"
}
}
}
This is Swift 5 correct syntax
enum ContentType: String {
case tables = "Tables"
case chairs = "Chairs"
}
let originalValues: [String: [String: String]]
= ["1234": ["Tables": "5", "Chairs": "2"]]
let values: [Int: [ContentType: Int]] = Dictionary(uniqueKeysWithValues:
originalValues.map { arg in
let (key, innerDict) = arg
let outMap: [ContentType: Int] = Dictionary(uniqueKeysWithValues:
innerDict.map { innerArg in
let (innerKey, innerValue) = innerArg
return (ContentType.init(rawValue: innerKey)!, Int(innerValue)!)
}
)
return (Int(key)!, outMap)
}
)
print(values)
[1234: [__lldb_expr_5.ContentType.tables: 5, __lldb_expr_5.ContentType.chairs: 2]]

Swift Generic does not show nil

I have following general structure where data can be anyother codable object
struct GeneralResponse<T:Codable>: Codable {
let message: String
let status: Bool
let data: T?
enum CodingKeys: String, CodingKey {
case message = "Message"
case status = "Status"
case data = "Data"
}
}
I have Following Like response codable class which will be used as data in GeneralResponse
class ImgLike: Codable {
let id: Int?
let imageID, user: String?
#available(*, deprecated, message: "Do not use.")
private init() {
fatalError("Swift 4.1")
}
enum CodingKeys: String, CodingKey {
case id = "ID"
case imageID = "ImageID"
case user = "User"
}
}
Question 1 : When the token expires on API, The response data is empty {} still It show ImgLike object with all nil properties. Why it not show data to be nil ?
Then If I check object?.data == nil it is showing false !! So I need to check each property
Question 2 : In ImgLike If I am using custom encode function. GeneralResponse not parsed with ImgLike not parsed it shows error in catch statement
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
imageID = try values.decode(String.self, forKey: .imageID)
user = try values.decode(String.self, forKey: .user)
do {
id = Int(try values.decode(String.self, forKey: .id))
} catch {
id = try values.decode(Int.self, forKey: .id)
}
}
The equivalents of Swift-nil are JSON-null and JSON-not-set. {} is a valid dictionary in JSON and so not Swift-nil.
I guess that you mean that you get an error incase you use the custom decoder function? That‘s expected since the default decoder uses decodeIfPresent instead of decode to decode optionals since they are allowed not to be set.
And since you decode an empty dictionary {} none of the values are present/set.
Counting keys in dict to avoid decoding from JSON-{}
This CodingKey-struct accepts every key it gets.
fileprivate struct AllKeysAllowed: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
}
struct GeneralResponse<T:Codable>: Decodable {
let message: String
let status: Bool
let data: T?
enum CodingKeys: String, CodingKey {
case message = "Message"
case status = "Status"
case data = "Data"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
message = try container.decode(String.self, forKey: .message)
status = try container.decode(Bool.self, forKey: .status)
Decode .data to a container which had all keys accepted.
Then the number of keys in the JSON-dictionary is readable with dataContainer.allKeys.count.
let dataContainer = try container.nestedContainer(keyedBy: AllKeysAllowed.self, forKey: .data)
if dataContainer.allKeys.count != 0 {
data = try container.decode(T.self, forKey: .data)
} else {
data = nil
}
}
}
Note that the default Codable implementation uses decodeIfPresent instead of decode. decodeIfPresent will not throw an error even if the key is not present in the JSON. It will simply return nil. So an empty JSON dictionary has no KVPs, so all the properties are set to nil.
In your custom implementation of Codable, you are using decode, which will throw an error if the key is not found.
The reason why object?.data != nil is because object?.data is a ImgLike???. You are wrapping an optional in an optional in an optional. I see that the type of object is GeneralResponse<ImgLike?>?. This will make data's type be ImgLike??. I don't think this is your intention. You probably intended to use GeneralRepsonse<ImgLike>. You might have forgotten to unwrap an optional somewhere. You also need to unwrap the outermost optional:
if let nonNilObject = object {
// nonNilObject.data is of type ImgLike?
}
As already mentioned the decoder does not treat an empty dictionary as nil.
You can add this functionality in a generic way with a tiny protocol and an extension of KeyedDecodingContainer
public protocol EmptyDictionaryRepresentable {
associatedtype CodingKeys : RawRepresentable where CodingKeys.RawValue == String
associatedtype CodingKeyType: CodingKey = Self.CodingKeys
}
extension KeyedDecodingContainer {
public func decodeIfPresent<T>(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T?
where T : Decodable & EmptyDictionaryRepresentable
{
guard contains(key) else { return nil }
let container = try nestedContainer(keyedBy: type.CodingKeyType.self, forKey: key)
return container.allKeys.isEmpty ? nil : try decode(T.self, forKey: key)
}
}
Just add EmptyDictionaryRepresentable conformance to ImgLike, the associated types are inferred.
class ImgLike: Codable, EmptyDictionaryRepresentable {
The properties in ImgLike could be even declared as non-optional

JSONEncoder won't allow type encoded to primitive value

I'm working on an implementation of Codable for an enum type with possible associated values. Since these are unique to each case, I thought I could get away with outputting them without keys during encoding, and then simply see what I can get back when decoding in order to restore the correct case.
Here's a very much trimmed down, contrived example demonstrating a sort of dynamically typed value:
enum MyValueError : Error { case invalidEncoding }
enum MyValue {
case bool(Bool)
case float(Float)
case integer(Int)
case string(String)
}
extension MyValue : Codable {
init(from theDecoder:Decoder) throws {
let theEncodedValue = try theDecoder.singleValueContainer()
if let theValue = try? theEncodedValue.decode(Bool.self) {
self = .bool(theValue)
} else if let theValue = try? theEncodedValue.decode(Float.self) {
self = .float(theValue)
} else if let theValue = try? theEncodedValue.decode(Int.self) {
self = .integer(theValue)
} else if let theValue = try? theEncodedValue.decode(String.self) {
self = .string(theValue)
} else { throw MyValueError.invalidEncoding }
}
func encode(to theEncoder:Encoder) throws {
var theEncodedValue = theEncoder.singleValueContainer()
switch self {
case .bool(let theValue):
try theEncodedValue.encode(theValue)
case .float(let theValue):
try theEncodedValue.encode(theValue)
case .integer(let theValue):
try theEncodedValue.encode(theValue)
case .string(let theValue):
try theEncodedValue.encode(theValue)
}
}
}
let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)
However, this is giving me an error during the encoding stage as follows:
"Top-level MyValue encoded as number JSON fragment."
The issue appears to be that, for whatever reason, the JSONEncoder won't allow a top-level type that isn't a recognised primitive to be encoded as a single primitive value. If I change the singleValueContainer() to an unkeyedContainer() then it works just fine, except that of course the resulting JSON is an array, not a single value, or I can use a keyed container but this produces an object with the added overhead of a key.
Is what I'm trying to do here impossible with a single value container? If not, is there some workaround that I can use instead?
My aim was to make my type Codable with a minimum of overhead, and not just as JSON (the solution should support any valid Encoder/Decoder).
There is a bug report for this:
https://bugs.swift.org/browse/SR-6163
SR-6163: JSONDecoder cannot decode RFC 7159 JSON
Basically, since RFC-7159, a value like 123 is valid JSON, but JSONDecoder won't support it. You may follow up on the bug report to see any future fixes on this. [The bug was fixed starting in iOS 13.]
#Where it fails#
It fails in the following line of code, where you can see that if the object is not an array nor dictionary, it will fail:
https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONSerialization.swift#L120
open class JSONSerialization : NSObject {
//...
// top level object must be an Swift.Array or Swift.Dictionary
guard obj is [Any?] || obj is [String: Any?] else {
return false
}
//...
}
#Workaround#
You may use JSONSerialization, with the option: .allowFragments:
let jsonText = "123"
let data = Data(jsonText.utf8)
do {
let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(myString)
}
catch {
print(error)
}
Encoding to key-value pairs
Finally, you could also have your JSON objects look like this:
{ "integer": 123456 }
or
{ "string": "potatoe" }
For this, you would need to do something like this:
import Foundation
enum MyValue {
case integer(Int)
case string(String)
}
extension MyValue: Codable {
enum CodingError: Error {
case decoding(String)
}
enum CodableKeys: String, CodingKey {
case integer
case string
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodableKeys.self)
if let integer = try? values.decode(Int.self, forKey: .integer) {
self = .integer(integer)
return
}
if let string = try? values.decode(String.self, forKey: .string) {
self = .string(string)
return
}
throw CodingError.decoding("Decoding Failed")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodableKeys.self)
switch self {
case let .integer(i):
try container.encode(i, forKey: .integer)
case let .string(s):
try container.encode(s, forKey: .string)
}
}
}
let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
print(theEncodedString!) // { "integer": 123456 }
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

Swift Codable - Parse JSON array which can contain different data type

I am trying to parse a JSON array which can be
{
"config_data": [
{
"name": "illuminate",
"config_title": "Blink"
},
{
"name": "shoot",
"config_title": "Fire"
}
]
}
or it can be of following type
{
"config_data": [
"illuminate",
"shoot"
]
}
or even
{
"config_data": [
25,
100
]
}
So to parse this using JSONDecoder I created a struct as follows -
Struct Model: Codable {
var config_data: [Any]?
enum CodingKeys: String, CodingKey {
case config_data = "config_data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
config_data = try values.decode([Any].self, forKey: .config_data)
}
}
But this would not work since Any does not confirm to decodable protocol. What could be the solution for this. The array can contain any kind of data
I used quicktype to infer the type of config_data and it suggested an enum with separate cases for your object, string, and integer values:
struct ConfigData {
let configData: [ConfigDatumElement]
}
enum ConfigDatumElement {
case configDatumClass(ConfigDatumClass)
case integer(Int)
case string(String)
}
struct ConfigDatumClass {
let name, configTitle: String
}
Here's the complete code example. It's a bit tricky to decode the enum but quicktype helps you out there:
// To parse the JSON, add this file to your project and do:
//
// let configData = try? JSONDecoder().decode(ConfigData.self, from: jsonData)
import Foundation
struct ConfigData: Codable {
let configData: [ConfigDatumElement]
enum CodingKeys: String, CodingKey {
case configData = "config_data"
}
}
enum ConfigDatumElement: Codable {
case configDatumClass(ConfigDatumClass)
case integer(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Int.self) {
self = .integer(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(ConfigDatumClass.self) {
self = .configDatumClass(x)
return
}
throw DecodingError.typeMismatch(ConfigDatumElement.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ConfigDatumElement"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .configDatumClass(let x):
try container.encode(x)
case .integer(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}
}
struct ConfigDatumClass: Codable {
let name, configTitle: String
enum CodingKeys: String, CodingKey {
case name
case configTitle = "config_title"
}
}
It's nice to use the enum because you get the most type-safety that way. The other answers seem to lose this.
Using quicktype's convenience initializers option, a working code sample is:
let data = try ConfigData("""
{
"config_data": [
{
"name": "illuminate",
"config_title": "Blink"
},
{
"name": "shoot",
"config_title": "Fire"
},
"illuminate",
"shoot",
25,
100
]
}
""")
for item in data.configData {
switch item {
case .configDatumClass(let d):
print("It's a class:", d)
case .integer(let i):
print("It's an int:", i)
case .string(let s):
print("It's a string:", s)
}
}
This prints:
It's a class: ConfigDatumClass(name: "illuminate", configTitle: "Blink")
It's a class: ConfigDatumClass(name: "shoot", configTitle: "Fire")
It's a string: illuminate
It's a string: shoot
It's an int: 25
It's an int: 100
You first need to decide what to do if the second JSON comes up. The second JSON format has way less info. What do you want to do with those data (config_title) that you lost? Do you actually need them at all?
If you do need to store the config_titles if they are present, then I suggest you to create a ConfigItem struct, which looks like this:
struct ConfigItem: Codable {
let name: String
let configTitle: String?
init(name: String, configTitle: String? = nil) {
self.name = name
self.configTitle = configTitle
}
// encode and init(decoder:) here...
// ...
}
Implement the required encode and init(decoder:) methods. You know the drill.
Now, when you are decoding your JSON, decode the config_data key as usual. But this time, instead of using an [Any], you can decode to [ConfigItem]! Obviously this won't always work because the JSON can sometimes be in the second form. So you catch any error thrown from that and decode config_data using [String] instead. Then, map the string array to a bunch of ConfigItems!
You are trying to JSON to object or object to JSON ? you can try this code add any swift file:
extension String {
var xl_json: Any? {
if let data = data(using: String.Encoding.utf8) {
return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
}
return nil
}
}
extension Array {
var xl_json: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
extension Dictionary {
var xl_json: String? {
guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
and run this code:
let str = "{\"key\": \"Value\"}"
let dict = str.xl_json as! [String: String] // JSON to Objc
let json = dict.xl_json // Objc to JSON
print("jsonStr - \(str)")
print("objc - \(dict)")
print("jsonStr - \(json ?? "nil")")
Finally, you'll get it:
jsonStr - {"key": "Value"}
objc - ["key": "Value"]
jsonStr - {"key":"Value"}

Swift 4 Decodable - Dictionary with enum as key

My data structure has an enum as a key, I would expect the below to decode automatically. Is this a bug or some configuration issue?
import Foundation
enum AnEnum: String, Codable {
case enumValue
}
struct AStruct: Codable {
let dictionary: [AnEnum: String]
}
let jsonDict = ["dictionary": ["enumValue": "someString"]]
let data = try! JSONSerialization.data(withJSONObject: jsonDict, options: .prettyPrinted)
let decoder = JSONDecoder()
do {
try decoder.decode(AStruct.self, from: data)
} catch {
print(error)
}
The error I get is this, seems to confuse the dict with an array.
typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath:
[Optional(__lldb_expr_85.AStruct.(CodingKeys in
_0E2FD0A9B523101D0DCD67578F72D1DD).dictionary)], debugDescription: "Expected to decode Array but found a dictionary instead."))
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")))
In order to solve your problem, you can use one of the two following Playground code snippets.
#1. Using Decodable's init(from:) initializer
import Foundation
enum AnEnum: String, Codable {
case enumValue
}
struct AStruct {
enum CodingKeys: String, CodingKey {
case dictionary
}
enum EnumKeys: String, CodingKey {
case enumValue
}
let dictionary: [AnEnum: String]
}
extension AStruct: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dictContainer = try container.nestedContainer(keyedBy: EnumKeys.self, forKey: .dictionary)
var dictionary = [AnEnum: String]()
for enumKey in dictContainer.allKeys {
guard let anEnum = AnEnum(rawValue: enumKey.rawValue) else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to an AnEnum object")
throw DecodingError.dataCorrupted(context)
}
let value = try dictContainer.decode(String.self, forKey: enumKey)
dictionary[anEnum] = value
}
self.dictionary = dictionary
}
}
Usage:
let jsonString = """
{
"dictionary" : {
"enumValue" : "someString"
}
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let aStruct = try! decoder.decode(AStruct.self, from: data)
dump(aStruct)
/*
prints:
▿ __lldb_expr_148.AStruct
▿ dictionary: 1 key/value pair
▿ (2 elements)
- key: __lldb_expr_148.AnEnum.enumValue
- value: "someString"
*/
#2. Using KeyedDecodingContainerProtocol's decode(_:forKey:) method
import Foundation
public enum AnEnum: String, Codable {
case enumValue
}
struct AStruct: Decodable {
enum CodingKeys: String, CodingKey {
case dictionary
}
let dictionary: [AnEnum: String]
}
public extension KeyedDecodingContainer {
public func decode(_ type: [AnEnum: String].Type, forKey key: Key) throws -> [AnEnum: String] {
let stringDictionary = try self.decode([String: String].self, forKey: key)
var dictionary = [AnEnum: String]()
for (key, value) in stringDictionary {
guard let anEnum = AnEnum(rawValue: key) else {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to an AnEnum object")
throw DecodingError.dataCorrupted(context)
}
dictionary[anEnum] = value
}
return dictionary
}
}
Usage:
let jsonString = """
{
"dictionary" : {
"enumValue" : "someString"
}
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let aStruct = try! decoder.decode(AStruct.self, from: data)
dump(aStruct)
/*
prints:
▿ __lldb_expr_148.AStruct
▿ dictionary: 1 key/value pair
▿ (2 elements)
- key: __lldb_expr_148.AnEnum.enumValue
- value: "someString"
*/
In Swift 5.6 (Xcode 13.3) SE-0320 CodingKeyRepresentable has been implemented which solves the issue.
It adds implicit support for dictionaries keyed by enums conforming to RawRepresentable with Int and String raw values.
Following from Imanou's answer, and going super generic. This will convert any RawRepresentable enum keyed dictionary. No further code required in the Decodable items.
public extension KeyedDecodingContainer
{
func decode<K, V, R>(_ type: [K:V].Type, forKey key: Key) throws -> [K:V]
where K: RawRepresentable, K: Decodable, K.RawValue == R,
V: Decodable,
R: Decodable, R: Hashable
{
let rawDictionary = try self.decode([R: V].self, forKey: key)
var dictionary = [K: V]()
for (key, value) in rawDictionary {
guard let enumKey = K(rawValue: key) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath,
debugDescription: "Could not parse json key \(key) to a \(K.self) enum"))
}
dictionary[enumKey] = value
}
return dictionary
}
}
Following Giles's answer, here is the same idea, but in the other direction, for encoding
public extension KeyedEncodingContainer {
mutating func encode<K, V, R>(_ value: [K: V], forKey key: Key) throws
where K: RawRepresentable, K: Encodable, K.RawValue == R,
V: Encodable,
R: Encodable, R: Hashable {
try self.encode(
Dictionary(uniqueKeysWithValues: value.map { ($0.key.rawValue, $0.value) }),
forKey: key
)
}
mutating func encodeIfPresent<K, V, R>(_ value: [K: V]?, forKey key: Key) throws
where K: RawRepresentable, K: Encodable, K.RawValue == R,
V: Encodable,
R: Encodable, R: Hashable {
if let value = value {
try self.encode(value, forKey: key)
}
}
}