Codable enum's associated value is unexpectedly using positional name

I have an enum something like:

enum Response: Codable {
    case error(message: String)
    case hello(username: String, version: String)
    case list([String: [String: ListUser]]) // struct ListUser is Codable and defined elsewhere
    // more cases but that doesn't matter
    
    enum CodingKeys: String, CodingKey {
        case error = "Error"
        case hello = "Hello"
        case list = "List"
    }

    // no custom init
}

The JSON I'm trying to decode would be something like:

{"Error": {"message": ""}}
{"Hello": {"username": "", "version": ""}}
{"List": {"room": {"user": {"version": ""}}}}

Note how the .list case involves a JSON object that maps as a dictionary, not an struct/associated values.

The default synthesized initializer works fine for for .hello and .error, but if I try to decode .list, I get an error like:

▿ Swift.DecodingError.keyNotFound
  ▿ keyNotFound: (2 elements)
    - .0: ListCodingKeys(stringValue: "_0", intValue: nil)
    ▿ .1: Swift.DecodingError.Context
      - codingPath: 0 elements
      - debugDescription: "No value associated with key ListCodingKeys(stringValue: \"_0\", intValue: nil) (\"_0\")."
      - underlyingError: nil

I think it's because with just a single value in the enum's associated value, Swift tries to use position-based naming (not well documented, I could only find the evolution proposal). I think it's expecting a structure like this:

{"List": {"_0": /* my [String: [String: ListUser]} */}}

...which does not seem terribly intuitive to me. Is there a way I can get it to do the right thing, without having to involve a custom initializer which means an all-or-nothing "I have to initialize every case manually" situation? (In this simple example, it doesn't seem too bad, but in my real enum, that would be even more complicated.)

1 Like

Try this one:

struct ListUser: Codable {
    var version: String
}

struct Room: Codable {
    var user: ListUser
}

enum Response: Codable {
    case error(message: String)
    case hello(username: String, version: String)
    case list(room: Room)
}

gives the wanted JSONs:

{"error":{"message":""}}
{"hello":{"username":"","version":""}}
{"list":{"room":{"user":{"version":""}}}}

Sorry, I don’t think I made myself clear with the dictionary part. There can be multiple objects in the dictionary like so:

{
    “List”: {
        “room1”: {“user1”: {“version”: “”}}, {“user2”: {“version”: “”}},
        “room2”: {“user3”: {“version”: “”}}, {“user4”: {“version”: “”}}
    }
}

(Sorry for smart quotes, I’m typing on my phone.)

That is, it’s not a fixed mapping, so treating it like one is wrong.

Then it's a little bit more work:

struct ListUser: Codable {
    var version: String
}

enum Response: Encodable {
    struct ErrorMessage: Encodable {
        var message: String
    }
    struct Hello: Encodable {
        var username: String
        var version: String
    }
    case error(ErrorMessage)
    case hello(Hello)
    case list(list: [String: [String: ListUser]])
    
    enum CodingKeys: String, CodingKey {
        case error = "Error"
        case hello = "Hello"
        case list = "List"
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .error(let message):
            try container.encode(message, forKey: .error)
        case .hello(let hello):
            try container.encode(hello, forKey: .hello)
        case .list(let list):
            try container.encode(list, forKey: .list)
        }
    }
}

Showing Encoding only, let me know if you need help with decoding.

I don't care too much about encoding at this point, though the example with encoding does confirm I do have to use a custom initializer for decoding, which is what I was expecting, but not liking. And probably switch these to using structs instead of named associated values instead, to make using the custom initializer more manageable.

(Maybe I can abuse putting the initializer in an extension to see if I can use the custom initializer only for the .list case and synthesize the rest in the enum....)

Another option is to use JSONSerializer, typically you do that when the amount of boilerplate using Codable is too much (although you'll have a different sort of boilerplate with JSONSerialization).

I was using JSONSerializer before JSONSerialization - that was too much boilerplate, unfortunately. JSONSerialization works pretty well, though every time I have to implement basic stuff in the custom initializer for a type when I just want to customize one thing gets annoying.

Ok, here's yet another option for you where you trade safety for boilerplate:

struct ListUser: Codable {
    var version: String
}

struct Response: Encodable {
    struct ErrorMessage: Encodable {
        var message: String
    }
    struct Hello: Encodable {
        var username: String
        var version: String
    }
    var error: ErrorMessage? = nil
    var hello: Hello? = nil
    var list: [String: [String: ListUser]]? = nil
}

let v1 = Response(error: .init(message: ""))
let v2 = Response(hello: .init(username: "", version: ""))
let v3 = Response(list: ["room" : ["user": ListUser(version: "")]])
{"error":{"message":""}}
{"hello":{"username":"","version":""}}
{"list":{"room":{"user":{"version":""}}}}

No boilerplate and encoding decoding done out of the box. Just be careful to not pass more than one thing in the Response initialiser.

Here's my solution for decoding. There may well be better answers, but it does work

struct ListUser: Decodable, CustomStringConvertible
{
	var version: String
	var description: String { "{ version: \(version) }" }
}

enum Response
{
	case error(message: String)
	case hello(username: String, version: String)
	case list([String: [String: ListUser]]) // struct ListUser is Codable and defined elsewhere
	// more cases but that doesn't matter

}

extension Response: Decodable
{
	enum CodingKeys: String, CodingKey
	{
		case error = "Error"
		case hello = "Hello"
		case list = "List"
	}

	enum Error: Swift.Error
	{
		case missingKey(String)
	}
	init(from decoder: Decoder) throws
	{
		let container = try decoder.container(keyedBy: CodingKeys.self)
		if let message = try container.decodeIfPresent(Dictionary<String, String>.self, forKey: .error)
		{
			guard let text = message["message"] else { throw Error.missingKey("message") }
			self = .error(message:text)
		}
		else if let hello = try container.decodeIfPresent(Dictionary<String, String>.self, forKey: .hello)
		{
			guard let userName = hello["username"] else { throw Error.missingKey("username") }
			guard let version = hello["version"] else { throw Error.missingKey("version") }
			self = .hello(username: userName, version: version)
		}
		else
		{
			let list = try container.decode(Dictionary<String, Dictionary<String, ListUser>>.self, forKey: .list)
			self = .list(list)
		}
	}
}
var decoder = JSONDecoder()

let list = try decoder.decode(Response.self, from: #"{"List": {"room": {"user": {"version": "1"}}, "room2" : {"user2": {"version": "3"}}}}"#.data(using: .utf8)!)
print(list)
let error = try decoder.decode(Response.self, from: #"{"Error": {"message": "there is an error"}}"#.data(using: .utf8)!)
print(error)
let hello = try decoder.decode(Response.self, from: #"{"Hello": {"username": "jeremyp", "version": "1"}}"#.data(using: .utf8)!)
print(hello)

The output in a Playground is as follows:

list(["room": ["user": { version: 1 }], "room2": ["user2": { version: 3 }]])
error(message: "there is an error")
hello(username: "jeremyp", version: "1")

wrt the struct: I did try that before enum, but I didn't like the else if let mess and wanted pattern matching.

Jeremy's solution looks close to what I was going to do myself, though the dictionary part is spicy.

Note that that implementation also has this "if let ... else" ladder:

    if let message = try container.decodeIfPresent(Dictionary<String, String>.self, forKey: .error) {
        ...
    } else if let hello = try container.decodeIfPresent(Dictionary<String, String>.self, forKey: .hello) {
        ...
    // more "if let else" as needed
    } else {
        ...
    }

The only way to avoid it it to somehow use the key as a discriminant, pseudocode:

    let key = ... grab the key somehow ...
    switch key {
        case "Message": return try container.decode(Message.self, ...)
        case "Error": return try container.decode(ErrorMessage.self, ...)
        case "List": return try container.decode(List.self, ...)
        default: throw error
    }

You could use allKeys and pick the first one.

		guard let key = container.allKeys.first else {  throw Error.missingKey("any key") }
		switch key
		{
		case .error:
			<#code#>
		case .hello:
			<#code#>
		case .list:
			<#code#>
		}

The reason I used dictionaries for each individual case was to avoid declaring a struct for each case that would only be used for encoding and decoding.

What about

	init(from decoder: Decoder) throws
	{
		let container = try decoder.container(keyedBy: CodingKeys.self)
		if let message = try container.decodeIfPresent([String : String].self, forKey: .error)
		{
			guard let text = message["message"] else { throw Error.missingKey("message") }
			self = .error(message:text)
		}
		else if let hello = try container.decodeIfPresent([String : String].self, forKey: .hello)
		{
			guard let userName = hello["username"] else { throw Error.missingKey("username") }
			guard let version = hello["version"] else { throw Error.missingKey("version") }
			self = .hello(username: userName, version: version)
		}
		else
		{
			let list = try container.decode([String : [String : ListUser]].self, forKey: .list)
			self = .list(list)
		}
	}
}

Is that a big concern though to worry about? Compare the two fragments:

struct Hello: Encodable { var username, version: String }
case hello(Hello)
...
switch response {
    case .hello(let v):
        some_usage(v.userName, v.version)
}

and:

case hello(username: String, version: String)
...
switch response {
    case .hello(let userName, let version):
        some_usage(userName, version)
    ....
}

There's a bit more overhead in the first fragment but it is hardly significant.

In fact the second version is somewhat less DRY.
struct Hello: Encodable { var a, b, c, d, e, f: String }
case hello(Hello)
...
switch response {
    case .hello(let v):
        some_usage(v.a, v.b, v.c, v.d, v.e, v.f)
}

and:

case hello(a: String, b: String, c: String, d: String, e: String, f: String)
...
switch response {
    case let .hello(a, b, c, d, e, f): // extra repeating
        some_usage(a, b, c, d, e, f)
    ....
}

If you are going to use the types as the types for the associated values of the enum cases, then of course not. I was just trying to keep the existing API the same as in the question though. Even then it might have been better to create the types just for the decoding because then I wouldn't have needed the guard statements to protect from the dictionaries not having the required keys.

These are all options and can be used or not used depending on preference.

btw, in this example:

	else if let hello = try container.decodeIfPresent([String : String].self, forKey: .hello) {
		guard let userName = hello["username"] else { throw Error.missingKey("username") }
		guard let version = hello["version"] else { throw Error.missingKey("version") }
		self = .hello(username: userName, version: version)
	}

you could further reduce the bloat a bit by having a throwing getter operator:

	else if let v = try container.decodeIfPresent([String : String].self, forKey: .hello) {
		self = try .hello(username: v.value("username"), version: v.value("version"))
	}

where:

extension Dictionary {
    func value(_ key: Key) throws -> Value {
        guard let value = self[key] else {
            throw Error.missingKey(key) 
        }
        return value
    }
}

I'm still worried about a ladder of "else if let xxx = try container.decodeIfPresent(". For example if there are many such variants, then what can happen is: we are staring decoding, perhaps even decoding a few fields and then dropping the partially decoded data half way because there are no needed fields, throwing the partially decoded data away, and then repeating this process N times... this sounds neither fast nor "right"... hence I'm gravitating towards the "switch key" approaches that jumps straight to the correct branch and uses "decode" vs "decodeIfPresent"; although admittedly I haven't seen such approaches used in practice.

I agree that it would be an issue in larger examples. It's not a problem in this case because only the last else will accept anything more than one levee deep. You could do something much more clever by using allKeys to find out what the keys are and then using a switch. You could then even throw an error if you have more than one key. My if ... else if ... code just ignores excess keys.

I think I'll just move the associated values into a struct and a custom initializer. It's unfortunate there's no in-between for automatic and fully manual initialization.

The following version gives the least amount of boilerplate (or so it seems, ~ 15 lines):

enum Response: Codable {
    case Error(ErrorRecord)
    case Hello(HelloRecord)
    case List(ListRecord)
    
    struct ErrorRecord: Codable { var message: String }
    struct HelloRecord: Codable { var username: String; var version: String }
    struct ListUser: Codable { var version: String }
    typealias ListRecord = [String: [String: ListUser]]

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        guard let key = c.allKeys.first else { throw NSError(...) }
        switch key {
        case .Error: self = .Error(try c.decode(ErrorRecord.self, forKey: key))
        case .Hello: self = .Hello(try c.decode(HelloRecord.self, forKey: key))
        case .List: self = .List(try c.decode(ListRecord.self, forKey: key))
        }
    }
}

To further reduce the boilerplate I switched to using case names matching your keys in JSON – no CodingKeys enum needed in this case – less boilerplate.

Note that this is decoding only, will take a few more lines with encoding.

Edit: edited code above to remove unneeded default case handling.

I think I would keep the CodingKeys enum because those capitalised enum cases would trigger me everywhere else in the code. Ir would also mean you don't need the default case in the switch.