35

I have the following Swift code

func doStuff<T: Encodable>(payload: [String: T]) {
    let jsonData = try! JSONEncoder().encode(payload)
    // Write to file
}

var things: [String: Encodable] = [
    "Hello": "World!",
    "answer": 42,
]

doStuff(payload: things)

results in the error

Value of protocol type 'Encodable' cannot conform to 'Encodable'; only struct/enum/class types can conform to protocols

How to fix? I guess I need to change the type of things, but I don't know what to.

Additional info:

If I change doStuff to not be generic, I simply get the same problem in that function

func doStuff(payload: [String: Encodable]) {
    let jsonData = try! JSONEncoder().encode(payload) // Problem is now here
    // Write to file
}
Mathias Bak
  • 4,687
  • 4
  • 32
  • 42
  • What is your goal? What you are trying to achieve? What are you going to do with your dictionary values being a Protocol instead of a type? – Leo Dabus Jun 07 '20 at 23:15
  • @matt My original question probably wasn't clear enough. I need to serialize the parameter to `doStuff`. If I change the signature to what you suggest, I'll get exactly the same problem as I described, just when I call the encode function. – Mathias Bak Jun 08 '20 at 09:05

4 Answers4

25

Encodable cannot be used as an annotated type. It can be only used as a generic constraint. And JSONEncoder can encode only concrete types.

The function

func doStuff<T: Encodable>(payload: [String: T]) {

is correct but you cannot call the function with [String: Encodable] because a protocol cannot conform to itself. That's exactly what the error message says.


The main problem is that the real type of things is [String:Any] and Any cannot be encoded.

You have to serialize things with JSONSerialization or create a helper struct.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • 2
    Thanks. I was afraid of this. Having a helper struct limits me greatly as I just want to encode and dump arbitrary data (which conforms to the Encodable protocol). – Mathias Bak Jun 08 '20 at 09:27
  • 5
    Isn't the whole point of a `protocol` that it declares a function so it can be called anytime? If it has a function like `open func encode(_ value: T) throws -> Data where T : Encodable` I expect it to be callable. What else does `Swift` need? This entirely kills the whole idea of using interfaces in general. What's next, `description` will need a specific instance as well? – andras Mar 03 '21 at 08:22
  • @andras The problem is not the protocol itself, the problem is to use the protocol type as a generic type which is impossible. – vadian Mar 03 '21 at 08:48
  • 3
    @vadian I still cannot comprehend it. I am storing something of type `Encodable`, which has a function `encode` (it specifies this contract). It means all instances have a function `encode`. Which means I should be able to call it. – andras Mar 03 '21 at 09:02
  • 1
    @andras The generic `T` represents always a single concrete type at runtime. It cannot be a protocol. – vadian Mar 03 '21 at 09:23
  • 4
    @vadian that would be the whole point of `protocol`, so we don't have to care about the single concrete type at runtime and still call the function it declares. If something is `Encodable`, it already implements `encode` (it has to). This means I cannot write `if let encodable = anyObject as? Encodable { try JSONEncoder().encode(payload) }` which is absurd to me. – andras Mar 03 '21 at 09:45
  • No it’s the same issue, the parameter of `encode` is a generic, the encoder needs a concrete type. Generics and protocols are two completely different things. – vadian Mar 03 '21 at 09:51
  • 4
    Thanks for the explanation but coming from the other language this language feature(limitation) sounds absolutely absurd to me. `[String: Encodable] because a protocol cannot conform to itself.` By specifying `[String: Encodable]` (at least in the language such as c# or typescript) we are not asking the interface (or protocol in this case) to conform to itself, we can't create instance of interface, so the value can only be an instance of struct/class/enum which conforms the protocol. – Norman Xu Dec 23 '21 at 21:07
6

You can use the where keyword combined with Value type, like this:

func doStuff<Value>(payload: Value) where Value : Encodable {
    ...
}
Ely
  • 8,259
  • 1
  • 54
  • 67
3

You're trying to conform T to Encodable which is not possible if T == Encodable. A protocol does not conform to itself.

Instead you can try:

func doStuff<T: Hashable>(with items: [T: Encodable]) {
    ...
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • 1
    The dictionary key is not the issue. It's the value (`Encodable`). When I pass that into another function I get the same issue. e.g. a json encoder. I've updated the question to reflect that. – Mathias Bak Jun 08 '20 at 09:08
3

What you are trying to do is possible with protocol extensions:

protocol JsonEncoding where Self: Encodable { }

extension JsonEncoding {
    func encode(using encoder: JSONEncoder) throws -> Data {
        try encoder.encode(self)
    }
}

extension Dictionary where Value == JsonEncoding {
    func encode(using encoder: JSONEncoder) throws -> [Key: String] {
        try compactMapValues {
            try String(data: $0.encode(using: encoder), encoding: .utf8)
        }
    }
}

Every type that could be in your Dictionary will need to conform to our JsonEncoding protocol. You used a String and an Int, so here are extensions that add conformance for those two types:

extension String: JsonEncoding { }
extension Int: JsonEncoding { }

And here is your code doing what you wanted it to do:

func doStuff(payload: [String: JsonEncoding]) {
    let encoder = JSONEncoder()
    do {
        let encodedValues = try payload.encode(using: encoder)
        let jsonData = try encoder.encode(encodedValues)
        
        // Write to file
    } catch {
        print(error)
    }
}

var things: [String: JsonEncoding] = [
    "Hello": "World!",
    "answer": 42
]

doStuff(payload: things)

You didn't ask about decoding, so I didn't address it here. You will either have to have knowledge of what type is associated with what key, or you will have to create an order in which you try to decode values (Should a 1 be turned into an Int or a Double...).

I answered your question without questioning your motives, but there is likely a better, more "Swifty" solution for what you are trying to do...

DudeOnRock
  • 3,685
  • 3
  • 27
  • 58