I just hit that problem as well...
I see the problem of archived data. However, IMHO the current behaviour is a bug - and data archived with it would be relying on that bug.
IMHO, dictionary is clearly a keyed structure, so I don't know how you can expect it to be encoded/decoded in an unkeyed container. Similarly to what @jjanke mentioned, I'd expect Dictionary to always encode itself into a keyed container.
I see, that there are limitations proposed by the resulting format. In this case JSON only being able to have string keys. But that is again something that the encoders/decoders should have to deal with. Since CodingKey already makes sure that keys of an unkeyed container can be represented as String and possibly Int, the encoders could make use of that.
The only remaining question is, how to get from an arbitrary RawRepresentable type (whose RawValue could be any type) to something that is convertible to String and/or Int.
I see two options here. One would be to implement some kind of marker, similar to what @itaiferber already described. In this case I'd just use a "dummy" coding key internally, instead of limiting it to String. This would allow the encoder/decoder to decide what to use as key (String or Int).
Another option would be to introduce a new protocol CodingKeyRepresentable, which would allow developers to decide themselves, how a RawRepresentable type (or any type for that matter) translates into a CodingKey:
public protocol CodingKeyRepresentable {
associatedtype CodingKey: Swift.CodingKey
var codingKey: CodingKey { get }
}
extension RawRepresentable where Self: CodingKeyRepresentable, Self.CodingKey == Self {
public var codingKey: CodingKey { return self }
}
extension RawRepresentable where Self: CodingKeyRepresentable, Self.CodingKey == RawValue {
public var codingKey: CodingKey { return rawValue }
}
With Swift 5.1's new features, we can maybe even provide some default implementation for any RawRepresentable type whose RawValue is either String or Int. (Haven't tried if that would work, though).
private enum AnyCodingKey: CodingKey {
case string(String)
case int(Int)
var stringValue: String {
switch self {
case .string(let str): return str
case .int(let int): return String(int)
}
}
var intValue: Int? {
switch self {
case .string(let str): return Int(str)
case .int(let int): return int
}
}
init?(stringValue: String) { self = .string(stringValue) }
init?(intValue: Int) { self = .int(intValue) }
}
extension RawRepresentable where Self: CodingKeyRepresentable, Self.RawValue == String {
public var codingKey: some Swift.CodingKey { return AnyCodingKey.string(rawValue) }
}
extension RawRepresentable where Self: CodingKeyRepresentable, Self.RawValue == Int {
public var codingKey: some Swift.CodingKey { return AnyCodingKey.int(rawValue) }
}
Otherwise we could simply not restrain the CodingKeyRepresentable protocol with an associatedtype and have its codingKey property simply return an existential. In this case it would be easier to provide default implementations.
Personally, I'd prefer the second approach, which would have to go through swift-evolution, though.
Looking at @Morten_Bek_Ditlevsen's PR, I'm not sure we should go down that road. It needs to introduce new public API in Foundation, which means it has to go through the API review @itaiferber mentioned, but still only fixes part of the problem for only one encoder/decoder. I think if we anyways have to go through a more in-depth review, we should at least have a solution that fixes the problem for good. 