OK, this is what I've got:
// The existential box is opened on `Encodable` methods,
// so we can get access to `Self: Encodable` instead of `Encodable`.
extension Encodable {
func encode(to jsonEncoder: JSONEncoder) throws -> Data {
try jsonEncoder.encode(self)
}
}
extension JSONEncoder {
func encode<T>(_ value: T, fallback: (Any) throws -> Data) throws -> Data {
guard let value = value as? Encodable else {
return try fallback(value)
}
// Just relay to our existential-opening proxy method.
return try value.encode(to: self)
}
}
extension Decodable {
// The same as for `Encodable`.
static func decode(to jsonDecoder: JSONDecoder, data: Data) throws -> Self {
try jsonDecoder.decode(self, from: data)
}
}
extension JSONDecoder {
func decode<T>(_ type: T.Type, from data: Data, fallback: (Data) throws -> T) throws -> T {
guard let decodableType = type as? Decodable.Type else {
return try fallback(data)
}
// Unfortunately, we do have to do an unsafe cast, but it's
// all in one place. We do the cast, get a `Decodable` box,
// operate on it and then cast in *one* function.
return try decodableType.decode(to: self, data: data) as! T
}
}
As I mentioned the above approach still uses an unsafe cast, which I haven't found a way to get rid of without SE-0309. So until Swift 5.7 we will have to bear an unsafe cast, but in the above case, the type erasure and casting happen in close proximity and are thus less error-prone.