Skip to content

Instantly share code, notes, and snippets.

@erikbasargin
Last active July 9, 2019 20:52
Show Gist options
  • Save erikbasargin/1c2820237761e6dee2641cfd78a320a1 to your computer and use it in GitHub Desktop.
Save erikbasargin/1c2820237761e6dee2641cfd78a320a1 to your computer and use it in GitHub Desktop.
Fix Publisher.decode for JSONDecoder and TopLevelDecoder πŸ› 
@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Publisher where Output == URLSession.DataTaskPublisher.Output {
public func decode<Item, Coder>(type: Item.Type, decoder: Coder) -> Publishers.TryMap<Self, Item> where Item: Decodable, Coder: TopLevelDecoder, Coder.Input == Data {
return tryMap { try decoder.decode(type, from: $0.data) }
}
}
@erikbasargin
Copy link
Author

If you look at the original code, you will see the problem: Self.Output == Coder.Input

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Publisher {

    /// Decodes the output from upstream using a specified `TopLevelDecoder`.
    /// For example, use `JSONDecoder`.
    public func decode<Item, Coder>(type: Item.Type, decoder: Coder) -> Publishers.Decode<Self, Item, Coder> where Item : Decodable, Coder : TopLevelDecoder, Self.Output == Coder.Input
}

JSONDecoder is extended by the TopLevelDecoder protocol.

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol TopLevelDecoder {

    associatedtype Input

    func decode<T>(_ type: T.Type, from: Self.Input) throws -> T where T : Decodable
}

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension JSONDecoder : TopLevelDecoder {
}

As a result, TopLevelDecoder.Input == Data. And then there is an error (in Publisher.decode):

error: instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'Data' be equivalent
    .decode(type: Object.self, decoder: JSONDecoder())

@erikbasargin
Copy link
Author

erikbasargin commented Jul 7, 2019

Example code with this error:

struct Repo: Codable, CustomStringConvertible {
    let full_name: String
    
    var description: String { return full_name }
}

let url = URL(string: "https://api.github.com/repositories")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTaskPublisher(for: request)
    .decode(type: [Repo].self, decoder: JSONDecoder()) // error: Instance method 'decode(type:decoder:)' requires the types 'URLSession.DataTaskPublisher.Output' (aka '(data: Data, response: URLResponse)') and 'Data' be equivalent
    .sink { (repos) in
        print("\(repos)")
    }

Someone solves the problem like this:

URLSession.shared.dataTaskPublisher(for: request)
    .tryMap({ return $0.data })
    .decode(type: [Repo].self, decoder: JSONDecoder())
    .sink { (repos) in
        print("\(repos)")
    }

Or like this:

class MyDecoder: TopLevelDecoder {
    private let jsonDecoder = JSONDecoder()

    typealias Input = URLSession.DataTaskPublisher.Output

    func decode<T: Decodable>(_ type: T.Type, from data: Input) throws -> T {
        return try self.jsonDecoder.decode(type, from: data.data)
    }
}

URLSession.shared.dataTaskPublisher(for: request)
    .decode(type: [Repo].self, decoder: MyDecoder())
    .sink { (repos) in
        print("\(repos)")
    }

But these are not fun solutions to the problem. That's why this gist appeared. Good luck! πŸ˜‰πŸ˜œ

@erikbasargin
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment