Neatly decoding the absence of data

I have a function that looks like this (simplified for the sake of the post):

func request<Response: Decodable>(url: URL, callback: (Result<Response, Error>) -> Void) {
    ...
}

This worked fine up until the point where I ran into an API that returned only status codes, and no response data.

How can I decode receiving no data successfully?

I tried Void as the generic parameter:

request(url: URL(string: "endpointWithNoResponse.com")!) { Void in // is that right? typing this on the go...

}

That didn't work, because Void isn't Decodable.

I tried

struct Empty: Decodable {}
request(url: URL(string: "endpointWithNoResponse.com")!) { (empty: Empty) in

}

but only really works if the data looks like this {}

So what's a nice, Swifty way to address this? Haven't tried Empty to handle nothing by overriding the init(decoder)... not sure if that's possible yet.

1 Like

Personally, I'd add an overload of the request function that omits the generic parameter and associated Decodable constraint:

func request(url: URL, callback: (Result<Void, Error>) -> Void))

(alternatively, drop Result and pass an Error? to the callback)

The downside to this is that it can lead to ambiguities during type inference/overload resolution, but since you're not using the generic parameter elsewhere in your signature, I'm guessing you have to manually write the types in the callback anyway. e.g:

request(url: URL(string: "blahblah")!) { (response: Result<String, Error>) in
  // ...
}

So if that's true, it would fit right in to your existing model and there shouldn't be any ambiguity.

1 Like

Another option is to have a version where nil is a valid result: Result<Response?, Error>, then callBack(.success(nil)).

This would likely still require overloading your request function, but can be made pretty generic with a where.

func request<Response: Decodable>(url: URL, callback: (Result<Response, Error>) -> Void) 
    where Response: ExpressibleByNilLiteral {
    ...
}

Alamofire includes an Empty type for exactly this purpose, as well as an EmptyResponse protocol, so that types can define their own empty value. In our included response serializers we check to see if an empty response is allowed, and, if so, attempt to cast the relevant empty type. It's not super elegant, but it lets us represent it in the type system.

Let me walk this back, actually. I think Result’s “success” case with a Void payload is much clearer than saying “a request whose callback is given a nil error implicitly succeeded”. Clearer APIs mean fewer mistakes.

1 Like

I agree, and tried to use it in Alamofire, but since Void doesn't conform to Decodable, it's tough to use in that context.

1 Like

I wonder if that will be added if/when we get Protocol conformance for Tuples :thinking: