Swift 4 Decodable - Dictionary With Enum as Key

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

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

How do I make an enum Decodable in Swift?

It's pretty easy, just use String or Int raw values which are implicitly assigned.

enum PostType: Int, Codable {
case image, blob
}

image is encoded to 0 and blob to 1

Or

enum PostType: String, Codable {
case image, blob
}

image is encoded to "image" and blob to "blob"


This is a simple example how to use it:

enum PostType : Int, Codable {
case count = 4
}

struct Post : Codable {
var type : PostType
}

let jsonString = "{\"type\": 4}"

let jsonData = Data(jsonString.utf8)

do {
let decoded = try JSONDecoder().decode(Post.self, from: jsonData)
print("decoded:", decoded.type)
} catch {
print(error)
}

Update

In iOS 13.3+ and macOS 15.1+ it's allowed to en-/decode fragments – single JSON values which are not wrapped in a collection type

let jsonString = "4"

let jsonData = Data(jsonString.utf8)
do {
let decoded = try JSONDecoder().decode(PostType.self, from: jsonData)
print("decoded:", decoded) // -> decoded: count
} catch {
print(error)
}

In Swift 5.5+ it's even possible to en-/decode enums with associated values without any extra code. The values are mapped to a dictionary and a parameter label must be specified for each associated value

enum Rotation: Codable {
case zAxis(angle: Double, speed: Int)
}

let jsonString = #"{"zAxis":{"angle":90,"speed":5}}"#

let jsonData = Data(jsonString.utf8)
do {
let decoded = try JSONDecoder().decode(Rotation.self, from: jsonData)
print("decoded:", decoded)
} catch {
print(error)
}

Swift codable recursive enum with dynamic keys

You can use singleValueContainer to decode/encode the each case of EntryData, without using any hardcoded keys. When decoding, we can try to decode as all three cases, and see which one succeeded.

enum EntryData: Codable {
case string(String)
case array([EntryData])
case nested([String: EntryData])

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let nested = try? container.decode([String: EntryData].self)
let array = try? container.decode([EntryData].self)
let string = try? container.decode(String.self)
switch (string, array, nested) {
case (let s?, nil, nil):
self = .string(s)
case (nil, let a?, nil):
self = .array(a)
case (nil, nil, let n?):
self = .nested(n)
default:
throw DecodingError.valueNotFound(
EntryData.self,
.init(codingPath: decoder.codingPath,
debugDescription: "Value must be either string, array or a dictionary!",
underlyingError: nil))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let s):
try container.encode(s)
case .array(let a):
try container.encode(a)
case .nested(let n):
try container.encode(n)
}
}
}

Now you can do:

let entry = try JSONDecoder().decode(Entry.self, from: data)

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.

When to use CodingKeys in Decodable(Swift)

First of all there is a make-or-break rule for using CodingKeys:

  • You can omit CodingKeys completely if the JSON – or whatever Codable conforming format – keys match exactly the corresponding properties (like in your example) or the conversion is covered by an appropriate keyDecodingStrategy.

  • Otherwise you have to specify all CodingKeys you need to be decoded (see also reason #3 below).


There are three major reasons to use CodingKeys:

  1. A Swift variable/property name must not start with a number. If a key does start with a number you have to specify a compatible CodingKey to be able to decode the key at all.
  2. You want to use a different property name.
  3. You want to exclude keys from being decoded for example an id property which is not in the JSON and is initialized with an UUID constant.

And CodingKeys are mandatory if you implement init(from decoder to decode a keyed container.

Swift decodable with programatically provided coding keys

It's possible to provide any contextual information to the decoder with userInfo property and in this case we can pass an array of coding keys and use this info in the decoding process:

struct Info: Decodable {
var text: String?
var num: Int?

static var keys = CodingUserInfoKey(rawValue: "keys")!

enum CodingKeys: String, CodingKey {
case text, num
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

guard let keys = decoder.userInfo[Self.keys] as? [CodingKeys] else {
return
}

if keys.contains(.text) {
text = try container.decode(String.self, forKey: .text)
}

if keys.contains(.num) {
num = try container.decode(Int.self, forKey: .num)
}
}
}


struct Root: Decodable {
let info: Info
}

let json = #"{ "info" : { "text": "Hello", "num": 20 } }"#.data(using: .utf8)!

let decoder = JSONDecoder()
let keys: [Info.CodingKeys] = [.text]
decoder.userInfo[Info.keys] = keys
let root = try decoder.decode(Root.self, from: json)
print(root)

// Outputs:
Root(info: Info(text: Optional("Hello"), num: nil))


Related Topics



Leave a reply



Submit