At least as of Swift 4.1, an enum with one or more cases with associated values cannot participate in synthesized Codable
conformance. For example, the following will yield compiler errors:
enum MyOtherEnum: Codable { // errors: does not conform to Encodable/Decodable
case foo
case bar(label: Int)
}
This is true even if all the associated values in said enum themselves conform to Codable
. In a similar fashion as the synthesized Hashable
/ Equatable
implementations for enums with associated values that themselves conform to Hashable
/ Equatable
, it would be useful for the Swift compiler to synthesize Codable
conformance automatically for any enum with associated values so long as every unique occurrence of an associated value type itself already conforms to Codable
.
In broad strokes, I'd expect the above code snippet to yield a synthesized implementation much like this:
enum MyEnum: Codable {
case foo
case bar(label: Int)
}
// Begin synthesized code...
extension MyEnum {
enum Discriminator: String, Decodable {
case foo, bar
}
enum CodingKeys: String, CodingKey {
case discriminator
case bar_label
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let discriminator = try container.decode(Discriminator.self, forKey: .discriminator)
switch discriminator {
case .foo:
self = .foo
case .bar:
let label = try container.decode(Int.self, forKey: .bar_label)
self = .bar(label: label)
}
}
func encode(to encoder: Encoder) throws {
// Yadda, yadda...
}
}
I have left out the encode(to:)
implementation as an exercise for the reader, since it should be an obvious inversion of the suggested init(from:)
implementation.
The algorithm for this synthesis of Decodable
conformance, in broad strokes:
- Synthesize a
Discriminator
enum that is a 1x1 mapping of the target enum's discriminators, but without any associated values, and using a rawString
type so it can participate in automaticCodable
conformance. - Synthesize the
CodingKeys
enum necessary to use a decoding container.CodingKeys
should start with a.discriminator
case which will be used to decode the target enum's discriminator. For each case of the target enum that has associated values, append a case toCodingKeys
for each associated value. If an associated value has a label, use the label as the key. If not, use the index of the associated value's position in the list of values (e.g.2
for the third position). Prefix the names of all the associated value keys with something predictable—perhaps the relevant discriminator—to prevent naming collisions between aCodingKey
and any associated value with an otherwise identical label (see the.anotherCase
example behind the GitHub Gist link below). Using the relevant discriminator from the target enum as a key prefix also prevents naming collisions between synthesizedCodingKeys
; the alternative would be to enforce uniquing of the synthesized keys, re-using keys across cases as needed. - Synthesize
init(from:)
, first by decoding the discriminator. Switch on the discriminator, implementing each case so that it attempts to decode only associated values for the relevant case of the target enum. There should already be the requiredCodingKey
cases necessary to perform this decoding. Once all associated values are decoded, if any, decoding is complete.
More verbose example available here in this GitHub Gist.
Given the following array:
let array: [MyEnum] = [.foo, .bar(label: 37)]
The JSON encoding of array
would be this:
[
{
"discriminator": "foo"
},
{
"discriminator": "bar",
"bar_label": 37
}
]