SE-0295: Codable synthesis for enums with associated values

I’m a strong -1 on this proposal as it is written currently.

The reason that SE-0166 left out encoding of enums with associated values is that unlike structs and RawRepresentable enums there is no obviously correct format for them. There have been a number of discussions about possible formats, e.g. in the thread linked to from the proposal, without a clear consensus emerging. As such, the problem to be solved here isn’t just to introduce a format for enums with associated values – but to introduce a format and show why that format is a better default than the many alternative formats that have been discussed, given the trade-offs involved. This proposal doesn’t do that.

One of the specific problems that I see with the suggested format is that it makes it hard to evolve schemas over time. In my experience it’s really common to want go from ”this can only be A” to ”this can be A or B”, i.e. from a struct to an enum. With the suggested format, this type of change is neither backward compatible nor forward compatible. With a format where the case name is encoded as its own key, this type of change can instead be made both backward compatible and forward compatible.

To illustrate, lets say we have a struct:

struct Foo: Codable {
  var a: Int
  var b: Int
}

This gets encoded like so:

{
  "a": 1,
  "b": 2
}

Now we want to evolve this to be an enum where the old Foo is one of several cases:

enum Bar: Codable {
  case foo(a: Int, b: Int)
  case baz(b: String)
}

With the proposed format, this would get encoded like so:

{
  "foo": {
    "a": 1,
    "b": 2
  }
}

This means that existing Foos encoded with earlier versions can’t be decoded as Bar.foos by the new version (the new version isn’t backward compatible) and that Bar.foos encoded with the new version can’t be decoded as Foos by earlier versions (earlier versions aren’t forward compatible).

If we instead encoded the name of the case as its own key, like so...

{
  "$case": "foo",
  "a": 1,
  "b": 2
}

...then Bar.foos encoded with the new version can automatically be decoded as Foos by earlier versions (they’re forward compatible). And if we default to the .foo case when the $case key is missing, then Foos encoded with earlier versions can be decoded as Bar.foos by the new version (it’s backward compatible).


It may well be that encoding the case name as its own key has other problems that the format proposed here doesn’t have. Perhaps the proposed format is really common among existing JSON APIs, and interoperability with those APIs is more important than the ability to evolve schemas over time? I don’t know. But whatever format we end up choosing will, by virtue of being the default, be used by many file formats and web services for years to come. That type of decision should rest on much stronger reasoning about the technical merits and trade-offs of the format than the proposal currently has.

At the very least, I think the proposal should compare the major alternative formats that have been suggested, and describe a) how easy or hard they make it to evolve schemas over time, and b) how common they are in the wild (both in handwritten Swift and in other languages’ serialization libraries, as well as in common APIs that Swift code might interoperate with).

10 Likes