SE-0295: Codable synthesis for enums with associated values

I think it's great that we're planning to define this stuff, but I'd like to suggest a couple refinements to the behavior.

Case with a single unlabeled associated value

The proposal suggests:

enum Command: Codable {
  case load(String)
  case store(String, value: Int)
}

would encoded to

{
  "load": [
    "MyKey"
  ]
}

Instead, I would recommend encoding this without the array wrapping it:

{
  "load": "MyKey"
}

This is a closer match for Serde's behavior (see the E::Y example). It also makes more sense with Swift's type system—the surrounding array implies that the associated value of Command.load is a 1-tuple, but there's no such thing as a 1-tuple in Swift. And it is just a cleaner representation of the value generally—internal objects, numbers, strings, or variable-length collections won't need to be wrapped in square brackets.

Case with no associated value

The proposal says:

An enum case without associated values would encode the same as one where all values have labels,
i.e.

enum Command: Codable {
  case dumpToDisk
}

would encode to:

{
  "dumpToDisk": {}
}

You justify this design by saying:

This is done for compatibility reasons. If associated values are added to a case later on, the structure
would not change, unless those values are unlabeled.

But I find this justification pretty thin. There is no particular reason to favor labeled values over unlabeled ones, and I don't think you've proposed any fallback behavior that would allow e.g. case dumpToDisk(volume: String) to decode { "dumpToDisk": {} } without an error.

Instead, I would recommend encoding it as a single flat string, not in a keyed container at all:

"dumpToDisk"

This again matches Serde's behavior more closely (see the E::Z example) and again is a cleaner expression of the value generally. However, it does have a couple of problems: it can't be used at the top level of a JSON document (which would need an object or array) and it wouldn't be expressed by CodingKeys. If you think you need something that works with a keyed container, an alternative would be:

{
  "dumpToDisk": true
}

The true here is a bit odd, as we will never write false there instead, but I think it captures the intent better than empty-object or empty-array.

Sub-coding keys

The proposal says:

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
  case load
  case store
}

// contains keys for all associated values of `case load`
enum CodingKeys_load: CodingKey {
  case key
}

// contains keys for all associated values of `case store`
enum CodingKeys_store: CodingKey {
  case key
  case value
}

I'm not sure if the compiler has the right capitalization routines to pull this off, but I'd love it if we could instead do:

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
  case load
  case store

  // contains keys for all associated values of `case load`
  enum Load: CodingKey {
    case key
  }

  // contains keys for all associated values of `case store`
  enum Store: CodingKey {
    case key
    case value
  }
}

Or at least:

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
  case load
  case store
}

// contains keys for all associated values of `case load`
enum LoadCodingKeys: CodingKey {
  case key
}

// contains keys for all associated values of `case store`
enum StoreCodingKeys: CodingKey {
  case key
  case value
}

My reasons here are purely aesthetic: I think these names are more like the names I would choose if I were writing the synthesized code by hand.

22 Likes