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!)
user438383
  • 5,716
  • 8
  • 28
  • 43
AnZ
  • 185
  • 1
  • 1
  • 9
  • Could try generic function as per [this](https://stackoverflow.com/questions/45053060/using-jsonencoder-to-encode-a-variable-with-codable-as-type) answer – logee Feb 21 '20 at 05:03

6 Answers6

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()
SPatel
  • 4,768
  • 4
  • 32
  • 51
  • Great ! It solve the problem perfectly! And what's the mean of `self` in `JSONEncoder().encode(self)` ? – AnZ Jun 27 '18 at 09:03
  • 3
    @Equal, toJSONData - method which is called by instance of type Encodable. Inside method the instance is referred as "self". – Eduard Jun 27 '18 at 09:29
  • Ooo nice way I never thought this was possible – J. Doe Jun 27 '18 at 09:44
  • I get it. Tanks! – AnZ Jun 27 '18 at 09:48
  • `Encodable` doesn't conform to `Encodable` ([protocols don't always conform to themselves](https://stackoverflow.com/a/43408193/2976878)), 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 https://forums.swift.org/t/how-to-encode-objects-of-unknown-type/12253/5?u=hamishknight for a way to make an `AnyEncodable` type erasing wrapper that can be passed to `encode(_:)`. – Hamish Jun 27 '18 at 15:58
  • You saved my day! – Natan R. Sep 16 '18 at 00:43
  • 1
    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) }` – River2202 May 02 '19 at 00:50
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!)
stevex
  • 5,589
  • 37
  • 52
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)
So Over It
  • 3,668
  • 3
  • 35
  • 46
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.

J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • 1
    i’m not really understand that why `JSONEncoder().encode() ` need a concrete class. There is no especial words in declaration of `encode()`. – AnZ Jun 27 '18 at 09:10
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)
Eduard
  • 475
  • 4
  • 14
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")
    }
}
Fattie
  • 27,874
  • 70
  • 431
  • 719