protocol codable crash

Hi


Similar question to https://forums.developer.apple.com/thread/79327?q=protocol%20codable I'm not sure if it's a bug or not.


Here is my scenario, best described in code...


import Foundation

public protocol MyProtocol : Codable {
    var name: String {
        get
        set
    }
}

public struct MyStruct : MyProtocol {
    public var name: String
    public var id: Int

    private enum CodingKeys : String, CodingKey {
        case id
        case name
    }

    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }

    /// Decode
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
    }

    /// Encode
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(name, forKey: .name)
    }
}

public class MyClass {
    public static func doSomething(completion: @escaping (MyProtocol)  -> Void) {
        let myStruct = MyStruct(id: 2, name: "Fred")
        completion(myStruct)
    }
}

MyClass.doSomething() {
    (result) in

    let value = try? JSONEncoder().encode(result)

    guard value == nil else {
        return
    }

    let json = String(data: value!, encoding: .utf8)
    print(json)
}


The error I get in playgrounds is:


Playground execution failed:

error: Samplecode.playground:43:36: error: cannot invoke 'encode' with an argument list of type '((MyProtocol))'

let value = try? JSONEncoder().encode(result)

^

Samplecode.playground:43:36: note: expected an argument list of type '(T)'

let value = try? JSONEncoder().encode(result)


If MyStruct doesn't implement init(from decoder: Decoder) or func encode(to encoder: Encoder) the compiler throws an error. So once these codable func's are implemented what is the compiler complaining about??


I've also made init(from decoder: Decoder) or func encode(to encoder: Encoder) as an extension to MyStruct, same error


Any ideas?

The fundamental problem here is that

result
, as declared on line 47, is of type
MyProtocol
, that is, a protocol not a concrete type. The encoder can’t encode a protocol, even if that protocol guarantees conformance to
Codable
; it can only encode a concrete type that conforms to
Codable
.

There are good reasons for this but I’m not confident enough in my own understanding to explain them myself. I can, however, point you in the right direction:

  • First, if

    P
    is a protocol then a value of type
    P
    does not conform to
    P
    . To learn more about this, see the Protocol doesn't conform to itself? reference in this Swift Forums post.
  • Second, the encoder has to know the concrete type so it can put that type in the archive and thus decode it properly.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn


The doSomething() func does create (line 41) and return concrete type (42) of Encodable implemented by MyProtocol. The only way I could get this to work in a generic way was to provide an extension to MyProtocol, as follows:


extension MyProtocol
{
    typealias T = Self
    func encode() -> Data? {
        return try? JSONEncoder().encode(self)
    }
}


The MyClass.doSomething completion would look like this:


MyClass.doSomething() {
    (result) in
    // let value = try? JSONEncoder().encode(result)
    // let json = String(data: value!, encoding: .utf8)
    let data = result.encode()
    let json = String(data: data!, encoding: .utf8)
    print(json)
}


The difference being that you couldn't use the JSONEncoder(),encode(result), instead call result.encode() which produces the desire JSON encoding.

What are your thoughts on this approach?

That’s fair enough. Essentially you’ve added a new requirement to your protocol, namely that values of that protocol must be able to serialise themselves in some way. That’s a perfectly reasonable protocol requirement. It’s hard to say anything beyond that without knowing more about the big picture of your program.

One other way to approach this problem is to make

MyClass.doSomething(…)
generic:
class MyClass {
    static func doSomething<T>(completion: @escaping (T)  -> Void) where T : MyProtocol {
        …
    }
}

MyClass.doSomething() { (result: MyStruct) in

    let value = try? JSONEncoder().encode(result)
    …
}

This works because the generic function operates in terms of a concrete type, albeit a concrete type that isn’t known at the point that the function is defined.

IMPORTANT Note line 8, where I have to tell

doSomething(…)
the concrete type of
result
.

As to which is better, again that really depends on the specifics of your program.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi Quinn


Thanks for the reply.


If I had serveral structs implementing the MyProtocol, then I'd need a callback function for each struct that maybe returned. For example the doSomething<T>() would need to be modified to:


public class MyClass {
    static func doSomething<T>(completion: @escaping (T)  -> Void) where T : MyProtocol {

        if someCondition {
            let myStruct = MyStructX(id: 1, name: "Fred") as! T   // MyStructX is new
            completion(myStruct)
       }
       else {
            let myStruct = MyStruct(id: 2, name: "Norm") as! T
            completion(myStruct)
      }
    }
}


Maybe something like:

MyClass.doSomething(completion: callbackForMyStruct)

func callbackForMyStruct(result: MyStruct)
{
  ...
}

MyClass.doSomething(completion: callbackForMyStructX)

func callbackForMyStructX(result: MyStructX)
{
  ...
}


Trying to inline the completion handler "binds" you into providing the concrete type, correct? i.e (result : concreteType). So add the extension func for the protocol or supply the concret type in the callbacks (above). Is this how you read it?


Many thanks

protocol codable crash
 
 
Q