This produces a JSON array containing both keys and values in alternating order, and not an object/dictionary of key-value pairs, as I would have expected:

[
"blue",
"0000ff",
"red",
"ff0000",
"green",
"00ff00"
]

Not all dictionaries behave this way, though. If we replace the enum keys with plain strings, the encoder produces a proper JSON object.

So Dictionary seems to behave differently depending on its Key type, even though the enum values are ultimately encoded as strings. What’s going on here? We can find the answer in Dictionary’s implementation for the Encodable protocol. The code looks like this:

extensionDictionary:Encodable/* where Key : Encodable, Value : Encodable */{publicfuncencode(toencoder:Encoder)throws{assertTypeIsEncodable(Key.self,in:type(of:self))assertTypeIsEncodable(Value.self,in:type(of:self))ifKey.self==String.self{// Since the keys are already Strings, we can use them as keys directly.varcontainer=encoder.container(keyedBy:_DictionaryCodingKey.self)for(key,value)inself{letcodingKey=_DictionaryCodingKey(stringValue:keyas!String)!try(valueas!Encodable).__encode(to:&container,forKey:codingKey)}}elseifKey.self==Int.self{// Since the keys are already Ints, we can use them as keys directly.varcontainer=encoder.container(keyedBy:_DictionaryCodingKey.self)for(key,value)inself{letcodingKey=_DictionaryCodingKey(intValue:keyas!Int)!try(valueas!Encodable).__encode(to:&container,forKey:codingKey)}}else{// Keys are Encodable but not Strings or Ints, so we cannot arbitrarily convert to keys.// We can encode as an array of alternating key-value pairs, though.varcontainer=encoder.unkeyedContainer()for(key,value)inself{try(keyas!Encodable).__encode(to:&container)try(valueas!Encodable).__encode(to:&container)}}}}

(The /* where Key : Encodable, Value : Encodable */ comment indicates constraints that should be there but can’t be expressed in the type system in Swift 4.0. The missing feature is called conditional conformance and will probably land in Swift 4.1 or 5.0. The two assertions at the beginning of the function ensure that the constraints hold at runtime.)

There are three branches: only if the dictionary’s key type is String or Int does it use a keyed container. Any other key type triggers results in an unkeyed container of alternating keys and values.

String and Int get special treatment because those are the two valid coding key types in the Codable world. Any other coding key is ultimately lowered to a String or Int. Since the dictionary has no way to tell how other types encode themselves, it has no choice but to resort to an unkeyed container.

And the fact that our custom enum is backed by String (and thus produces String values when encoded) doesn’t matter. The standard library can’t use a rule like “use a keyed container if Key: RawRepresentable, Key.RawValue == String“ because, however unlikely, the type may have overridden the default compiler-synthesized Codable implementation to encode itself differently — there’s no practical way for the standard library to tell.

I don’t think there’s a simple fix for this, short of manually converting any dictionary to String or Int keys before encoding. Here’s how you could do this for the example dictionary (this works for any RawRepresentable dictionary with a RawValue of String or Int):