The issue of automatic Codable
synthesis for enums with associated types has come up several times in this forum, most recently here: SE-0295: Codable synthesis for enums with associated values (this was recently returned for revision). It seems the main issues from this and prior proposals were that they were not customizable enough to support the full range of coding strategies folks were interested in, and that there wasn't agreement on a good global default.
I think we can solve both of these issues by introducing four new codable container types: Tagged{En,De}codingContainer<Tag>
, and Tuple{En,De}codingContainer<Tag>
. Here is an example of what this could look like for Decodable
:
enum Foo: Codable {
case int(Int)
case string(string: String)
case tuple(Int, String)
/// Cases with identical labels will prevent automatic synthesis
/// case tuple(String, Int)
case tupleWithLabel(Int, label: String)
/// Synthesized
enum CodingTag: Swift.CodingTag, String { /// This is synthesized similar to CodingKey
/// We could eventually expand the `CodingTag` protocol to handle cases which only differ by value labels
case int, string, tuple = "tupleWithoutLabel", tupleWithLabel
}
init(from decoder: Decoder) throws {
let container = try decoder.container(taggedBy: CodingTag.self)
switch container.tag {
/// Enums with a single associated value will decode via `singleValueDecodingContainer`
/// The inability to have a single-value tuple already has precedent in the language
case .int:
self = try .int(container.valueDecoder.singleValueDecodingContainer().decode(Int.self))
/// Everything else will decode as a tuple
case .string:
var valueContainer = container.valueDecoder.tupleContainer(count: 1)
self = try .string(valueContainer.decode(String.self, label: nil))
case .tuple:
var valueContainer = container.valueDecoder.tupleContainer(count: 2)
var value_0 = valueContainer.decode(Int.self, label: nil)
var value_1 = valueContainer.decode(String.self, label: nil)
self = try .tuple(value_0, value_1)
case .tupleWithLabel:
var valueContainer = container.valueDecoder.tupleContainer(count: 2)
var value_0 = valueContainer.decode(String.self, label: "label")
var value_1 = valueContainer.decode(Int.self, label: nil)
self = try .tuple(value_0, value_1)
}
}
}
With this infrastructure in place, we can provide some of the more popular defaults as decoding strategies. We would still have a true default, but it would be much easier to customize with something like TupleEncodingStrategy
and critically, the default would be per-decoder and we would be preserving the full type information in the generated initializer. This way we can have different defaults for JSON vs Plists if that makes sense. The synthesis for tuples could also extend to struct
stored properties with tuple types.
I haven't made any significant contributions to Swift yet, but I'm confident I could handle the Swift parts like modifying JSONDecoder
if someone would like to collaborate on the compiler bits for a reference implementation.
Postscript: While I think something like this could fit nicely into Codable
as it currently stands, it exacerbates some of the problems with Codable, most significantly the sheer amount of code it takes to create a custom Decoder
. I'd love to eventually see an approach that simplifies creating custom Decoder
s, and maybe even addresses some of the underlying performance issues of Codable
, but I wouldn't want to wait for that to have a good story for tuples and enums with associated values.