Skip to content

Instantly share code, notes, and snippets.

@stinger
Last active February 17, 2024 02:07
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save stinger/e8b706ab846a098783d68e5c3a4f0ea5 to your computer and use it in GitHub Desktop.
Save stinger/e8b706ab846a098783d68e5c3a4f0ea5 to your computer and use it in GitHub Desktop.
Combine - fetching and decoding JSON data
import Foundation
import Combine
enum APIError: Error, LocalizedError {
case unknown, apiError(reason: String), parserError(reason: String)
var errorDescription: String? {
switch self {
case .unknown:
return "Unknown error"
case .apiError(let reason), .parserError(let reason):
return reason
}
}
}
struct Fact: Decodable {
var text: String
}
func fetch(url: URL) -> AnyPublisher<Data, APIError> {
let request = URLRequest(url: url)
return URLSession.DataTaskPublisher(request: request, session: .shared)
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else {
throw APIError.unknown
}
return data
}
.mapError { error in
if let error = error as? APIError {
return error
} else {
return APIError.apiError(reason: error.localizedDescription)
}
}
.eraseToAnyPublisher()
}
func fetch<T: Decodable>(url: URL) -> AnyPublisher<T, APIError> {
fetch(url: url)
.decode(type: T.self, decoder: JSONDecoder())
.mapError { error in
if let error = error as? DecodingError {
var errorToReport = error.localizedDescription
switch error {
case .dataCorrupted(let context):
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".")
errorToReport = "\(context.debugDescription) - (\(details))"
case .keyNotFound(let key, let context):
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".")
errorToReport = "\(context.debugDescription) (key: \(key), \(details))"
case .typeMismatch(let type, let context), .valueNotFound(let type, let context):
let details = context.underlyingError?.localizedDescription ?? context.codingPath.map { $0.stringValue }.joined(separator: ".")
errorToReport = "\(context.debugDescription) (type: \(type), \(details))"
@unknown default:
break
}
return APIError.parserError(reason: errorToReport)
} else {
return APIError.apiError(reason: error.localizedDescription)
}
}
.eraseToAnyPublisher()
}
// Usage
if let url = URL(string: "https://cat-fact.herokuapp.com/facts/random") {
fetch(url: url)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure(let error):
print("Error: \(error.localizedDescription)")
}
}, receiveValue: { (fact: Fact) in
print(fact.text)
})
}
@ankitgogia
Copy link

Nicely coded example to implement separation of concern principles; keeps decoding and fetching logic modular/customisable.

@timothyjoel
Copy link

This is a great example of how to fetch and decode JSON using combine. Thanks!

@worchyld
Copy link

Could this repurposed/reused for loading local JSON files ?

@taimila
Copy link

taimila commented Aug 21, 2020

Good example snippet! Thanks for sharing.

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