I am trying to do some decoding of JSON I am receiving from a server. As this app is being written in SwiftUI I thought I may as well give Combine a go as well. I have been using .decode() as part of my combine chain which has been working well but now I need to decode json which won't work with this.
I am trying to decode JSON of a format into a Team
struct. However the issue is that this is not guaranteed to exist on the server, in those cases, the server simply returns no JSON (it does however still have the correct HTTPS response code so I do know when this is the case). My question is how to decode the received data into an optional Team?
(where it is either the decoded team data or nil if no JSON is received.
struct Team: Codable, Identifiable, Hashable {
var id: UUID
var name: String
var currentRating: Int
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case currentRating = "rating"
}
}
func fetch<T: Decodable>(
from endpoint: Endpoint,
with decoder: JSONDecoder = JSONDecoder()
) -> AnyPublisher<T, DatabaseError> {
// Get the URL from the endpoint
guard let url = endpoint.url else { ... }
let request = URLRequest(url: url)
// Get the publisher data from the server
// retrieveData is a function with the return type AnyPublisher<Data, DatabaseError>
return retrieveData(with: request)
// Try to decode into a decodable object
.decode(type: T.self, decoder: decoder)
// If there is an error, map it to a DatabaseError
.mapError { ... }
// Main thread
.receive(on: DispatchQueue.main)
// Type erase
.eraseToAnyPublisher()
}
Ideally, you can inspect the server response and decide what you want to do, say given a specific HTTP code or if data
is empty, but in your case, retrieveData
just gives you data - so, there isn't much to play with here.
What you could is attempt to decode, and if there's a failure, return nil
:
return retrieveData(with: request)
.flatMap {
Just($0)
.decode(type: T?.self, decoder: decoder)
.replaceError(with: nil)
}
.mapError { ... }
//... etc
The downside of the above is that it would hide any actual decoding errors, like type mismatches, so you could be more precise in your handling and only decode when data is not empty.
Here's a possible decodeIfPresent
implementation:
extension Publisher where Output == Data {
func decodeIfPresent<T: Decodable, Coder: TopLevelDecoder>(
type: T.Type,
decoder: Coder
) -> AnyPublisher<T?, Error> where Coder.Input == Output {
self.mapError { $0 as Error }
.flatMap { d -> AnyPublisher<T?, Error> in
if d.isEmpty {
return Just<T?>(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} else {
return Just(d)
.decode(type: T?.self, decoder: decoder)
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
}