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
Discriminatorenum that is a 1x1 mapping of the target enum's discriminators, but without any associated values, and using a rawStringtype so it can participate in automaticCodableconformance. - Synthesize the
CodingKeysenum necessary to use a decoding container.CodingKeysshould start with a.discriminatorcase 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 toCodingKeysfor 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.2for the third position). Prefix the names of all the associated value keys with something predictable—perhaps the relevant discriminator—to prevent naming collisions between aCodingKeyand any associated value with an otherwise identical label (see the.anotherCaseexample 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 requiredCodingKeycases 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
}
]