Why can't we use protocol `Encodable` as a type in the func?
Asked Answered
E

6

17

I'm trying to get data by encode model which conforms to Encodable protocol. But it's failed to invoke func encode like code below:

// MARK: - Demo2

class TestClass2: NSObject, Encodable {
    var x = 1
    var y = 2
}


var dataSource2: Encodable?

dataSource2 = TestClass2()

// error: `Cannot invoke 'encode' with an argument list of type '(Encodable)'`
let _ = try JSONEncoder().encode(dataSource2!)
//func encode<T>(_ value: T) throws -> Data where T : Encodable

But in another demo, it works well, why?

// MARK: - Demo1

protocol TestProtocol {
    func test()
}

class TestClass1: NSObject, TestProtocol {
    func test() {
        print("1")
    }

    var x = 1
    var y = 2
}


var dataSource1: TestProtocol?

dataSource1 = TestClass1()


func logItem(_ value: TestProtocol) {
    value.test()
}

logItem(dataSource1!)
Electoral answered 27/6, 2018 at 8:42 Comment(1)
Could try generic function as per this answerQuinones
C
38

Solution 1.

Try this code, which extend encodable

extension Encodable {
    func toJSONData() -> Data? { try? JSONEncoder().encode(self) }
}

Solution 2.

To avoid polluting Apple-provided protocols with extensions

protocol MyEncodable: Encodable {
    func toJSONData() -> Data?
}

extension MyEncodable {
    func toJSONData() -> Data?{ try? JSONEncoder().encode(self) }
}

Use

var dataSource2: Encodable?
dataSource2 = TestClass2()
let data = dataSource2?.toJSONData()
Carranza answered 27/6, 2018 at 8:52 Comment(7)
Great ! It solve the problem perfectly! And what's the mean of self in JSONEncoder().encode(self) ?Electoral
@Equal, toJSONData - method which is called by instance of type Encodable. Inside method the instance is referred as "self".Primitivism
Ooo nice way I never thought this was possibleIrony
I get it. Tanks!Electoral
Encodable doesn't conform to Encodable (protocols don't always conform to themselves), so encode(_:)'s generic T : Encodable parameter doesn't accept an Encodable argument. The protocol extension workaround works because Swift implicitly opens existentials when accessing members on protocol-typed values. See also forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/… for a way to make an AnyEncodable type erasing wrapper that can be passed to encode(_:).Theatrician
You saved my day!Scopophilia
This solution work very well and is a perfect solution for using Encodable variable without generic. I case is I am using a enum case .json(object: Encodable) but get an error when try to do switch case .json(let object) return try? JSONEncoder().encode(object) With this extension my code works with: switch case .json(let object) return object.jsonData BTW I change the extension to var jsonData: Data? { return try? JSONEncoder().encode(self) }Izmir
A
9

You can't pass a protocol but you can use generics to require a class that conforms to one:

func printJSON<T: Encodable>(_ data: T) {
    if let json = try? JSONEncoder().encode(data) {
        if let str = String(data: json, encoding: .utf8) {
            print(str)
        }
    }
}

// Now this should work
var dataSource2 = TestClass2()
printJSON(dataSource2!)
Aseity answered 31/10, 2019 at 11:0 Comment(0)
S
8

There are a number of approaches to solving this problem.

@SPatel solution of extending Encodable is one possibility. However, I personally try to avoid polluting Apple-provided protocols with extensions.

If I am reading between the lines, it appears what you are wanting is to pass any construct that conforms to Encodable to a function/method in some other struct/class.

Let's take an example of what I think you are trying to achieve:

struct Transform {
    static func toJson(encodable: Encodable) throws -> Data {
        return try JSONEncoder().encode(encodable)
    }
}

However, Xcode will complain:

Protocol type 'Encodable' cannot conform to 'Encodable' because only concrete types can conform to protocols

A Swift-ier solution is to use a constrained generic on the function:

struct Transform {
    static func toJson<EncodableType: Encodable>(encodable: EncodableType) throws -> Data {
        return try JSONEncoder().encode(encodable)
    }
}

Now the compiler can infer the type that conforms to Encodable, and we can call the function as intended:

let dataSource = TestClass2()
let jsonData = try? Transform.toJson(encodable: dataSource)
Shulock answered 28/6, 2019 at 23:18 Comment(1)
This is much cleaner! Thanks!Trisa
I
1

Your 2 examples are different.

JSONEncoder().encode() expects a concrete class which conforms to the procotol Encodable. The reference dataSource2 holds a protocol and not a concrete class.

logItem on the other hands, only takes a protocol as input, and NO concrete class which conforms to the protocol. This is the difference between your examples and why your second case is working and the first case does not.

With your current setup, it will not work. You need to pass in a concrete class to the JSONEncoder.

Irony answered 27/6, 2018 at 8:50 Comment(1)
i’m not really understand that why JSONEncoder().encode() need a concrete class. There is no especial words in declaration of encode().Electoral
P
0

As long as TestClass2 is Encodable you can use following code. encode should know what to encode. It refers to class properties to do that. Encodable itself doesn't contain any properties.

JSONEncoder().encode(dataSource2 as! TestClass2)
Primitivism answered 27/6, 2018 at 8:51 Comment(1)
I can't get the real type of TestClass2, it's can be any else class.Electoral
H
0

Could it be you're just looking for this ?

func blah<T>(_ thing: T) where T: Codable {
    do {
        let data = try JSONEncoder().encode(thing)
        .. do something with data
    } catch {
        print("severe problem in blah, couldn't encode")
    }
}
Herringbone answered 1/6, 2022 at 12:25 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.