Hi @itaiferber,
I have an implementation, but it did turn out to be more difficult than I originally imagined, and perhaps it is too messy.
My challenges were:
-
Checking if the current type conforms to the marker protocol is not quite enough. Once that you know that the type is a string keyed dictionary you still have to be able to create new instances and you need to know that the type of the value is
Decodable
. So instead of a pure 'marker' protocol, the protocol I ended up with has both aninit
and adecode
function. Then I can use casting of conditional conforming types of Swift 4.2 to use these functions. -
I tried any number of ways to 'short circuit' the boxing and unboxing without having go through the existing APIs - in order to avoid the encoding and decoding of the keys. For decoding I created a Dictionary manually, and unboxed each value of the container manually too. But all attempts to do this meant that the [CodingKey] path is not being updated, and so I would break error handling. I tried taking this a step further, reproducting the
box_
andunbox
behaviors, but these have overrides for many specific types, to I would end up duplicating a whole lot of code.
So my suggestion for a solution (which I do find suboptimal) is:
- Create a
CodingKey
conforming type intended for string keyed dictionaries. - In my 'marker' protocol I have alternative versions of
init(from decoder:)
andencode(to encoder:)
that performs the exact same code as the currentDictionary
Decodable
andEncodable
implementations (where the key is a String). - These implementations, however, use my new
CodingKey
conforming type (_JSONStringDictionaryCodingKey
). - Where the key encoding and key decoding is performed, I check if the type of the key is
_JSONStringDictionaryCodingKey
and skip the key coding if it is.
This implementation is very similar to a strategy where one could opt out from key encoding / decoding by conforming to a specific marker protocol. If such a strategy was implemented, then the existing _DictionaryCodingKey
could just conform to this protocol and most of the code in my current fix could be deleted.
I'd be glad to make a PR, but right now I have only tried out the implementation in a copy of the JSONEncoder/JSONDecoder classes, so it would need a bit of cleaning up + official tests. Perhaps you would like to comment if this is a desired direction before I make the PR?
Extensions to JSONEncoder.swift
:
fileprivate protocol _JSONStringDictionaryMarker {
init(_from decoder: _StructuralDecoder) throws
func encode_(to encoder: _StructuralEncoder) throws
}
/// A wrapper for dictionary keys which are Strings or Ints.
@usableFromInline // FIXME(sil-serialize-all)
@_fixed_layout // FIXME(sil-serialize-all)
internal struct _JSONStringDictionaryCodingKey : CodingKey {
@usableFromInline // FIXME(sil-serialize-all)
internal let stringValue: String
@usableFromInline // FIXME(sil-serialize-all)
internal let intValue: Int?
@inlinable // FIXME(sil-serialize-all)
internal init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}
@inlinable // FIXME(sil-serialize-all)
internal init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
}
extension Dictionary : _JSONStringDictionaryMarker where Key == String, Value: Decodable, Value: Encodable {
fileprivate init(_from decoder: _StructuralDecoder) throws {
self.init()
let container = try decoder.container(keyedBy: _JSONStringDictionaryCodingKey.self)
for key in container.allKeys {
let value = try container.decode(Value.self, forKey: key)
self[key.stringValue] = value
}
}
fileprivate func encode_(to encoder: _StructuralEncoder) throws {
var container = encoder.container(keyedBy: _JSONStringDictionaryCodingKey.self)
for (key, value) in self {
let codingKey = _JSONStringDictionaryCodingKey(stringValue: key)!
try container.encode(value, forKey: codingKey)
}
}
}
In _JSONKeyedEncodingContainer
:
private func _converted(_ key: CodingKey) -> CodingKey {
let useKeyEncoding = !(key is _JSONStringDictionaryCodingKey)
guard useKeyEncoding else {
return key
}
...
}
In _JSONKeyedDecodingContainer
:
/// Initializes `self` by referencing the given decoder and container.
fileprivate init(referencing decoder: _StructuralDecoder, wrapping container: [String : Any]) {
self.decoder = decoder
let useKeyEncoding = !(Key.self is _JSONStringDictionaryCodingKey.Type)
switch (decoder.options.keyDecodingStrategy, useKeyEncoding) {
case (.useDefaultKeys, _), (_, false):
self.container = container
case (.convertFromSnakeCase, true):
...
case (.custom(let converter), true):
...
}
...
}
In _JSONEncoder
, box_
:
...
// The value should request a container from the _StructuralEncoder.
let depth = self.storage.count
do {
if let stringKeyedDictionary = value as? _JSONStringDictionaryMarker {
try stringKeyedDictionary.encode_(to: self)
} else {
try value.encode(to: self)
}
} catch {
...
In _JSONDecoder
, unbox
:
...
} else if let stringKeyedDictType = type as? _JSONStringDictionaryMarker.Type {
self.storage.push(container: value)
defer { self.storage.popContainer() }
return try stringKeyedDictType.init(_from: self) as? T
} else {
self.storage.push(container: value)
defer { self.storage.popContainer() }
return try type.init(from: self)
}
As I said - I'm not too happy about this - it feels a bit hacky. But if it were a general protocol that CodingKey
conformers could also conform to, then most of this could go away and the existing _DictionaryCodingKey
could just be made to conform to this.
What do you think?