28

I have a generic REST request:

struct Request<T> {…}

The T is the return type of the request, for example:

struct Animal {…}
let animalRequest = Request<Animal>
let animal: Animal = sendRequest(animalRequest)

Now I would like to express that the generic type has to conform to Decodable so that I can decode the JSON response from the server:

struct Request<T> where T: Decodable {…}
struct Animal: Decodable {…}

This makes sense and works – until I arrive at a request that has no response, a Request<Void>. The compiler is not happy about that one:

Type 'Void' does not conform to protocol 'Decodable'

My mischevious attempt to solve this by adding the Decodable conformance to Void was quickly found out by the compiler:

extension Void: Decodable {…} // Error: Non-nominal type 'Void' cannot be extended

It feels right to have the request generic over the return type. Is there a way to make it work with Void return types? (For example the requests that just create something on the server and don’t return anything.)

zoul
  • 102,279
  • 44
  • 260
  • 354
  • Maybe I misunderstand the question but it's up to you – the developer – to take care of *a**void**ing* the `Void` requests – vadian Aug 11 '17 at 12:25
  • I can understand your point of view, but at the same time it feels right that if a thing is generic over _x_, then `Void` aka zero tuple `()` should be a valid value for _x_. After all it’s trivially `Equatable` and `Decodable`. – zoul May 21 '18 at 10:01
  • @zoul Still have to wonder what `Request` is supposed to mean. Why are you using things like that? If that's a response type, it is never `Void`. It can be empty but it's never `Void`. – Sulthan Jun 16 '18 at 16:48
  • 1
    What’s the difference between empty and `Void`? To me, there’s a perfect analogy in plain functions, a request returning `Void` is the same as a function returning `Void`. Both are only used for the side effects. – zoul Jun 17 '18 at 05:31

2 Answers2

41

A simple workaround is to introduce a custom “no-reply” type that would replace Void:

struct NoReply: Decodable {}

Conforming Void to Decodable is not possible. Void is just a type alias for an empty tuple, (), and tuples cannot conform to protocols at this moment, but they will, eventually.

Yakiv Kovalskyi
  • 1,737
  • 1
  • 15
  • 29
zoul
  • 102,279
  • 44
  • 260
  • 354
  • This solution is fine – iWheelBuy Jan 30 '18 at 12:21
  • amazing. Thanks! – Jan Apr 24 '18 at 21:15
  • 2
    This doesn't work for me, I get a DecodingError.dataCorrupted with context: `Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "No value." UserInfo={NSDebugDescription=No value.}))` – Marmoy Apr 16 '19 at 08:40
  • 5
    @Marmoy perhaps you're trying to decode an empty Data. JSONDecoder requires the input data to be a valid JSON document. As a workaround you can detect if the response data is empty and in that case replace it with a mocked empty json data: `let emptyJson = "{}".data(using: .utf8)` – Zimes May 27 '19 at 13:37
1

I found that sometimes other encoded objects of another types can be decoded to NoReply.self. For example custom Error type (enum) can be.

Playground example of this case:

enum MyError: String, Codable {
    case general
}

let voidInstance = VoidResult()
let errorInstance = MyError.general
let data1 = try! JSONEncoder().encode(voidInstance)
let data2 = try! JSONEncoder().encode(errorInstance)

let voidInstanceDecoded = try! JSONDecoder().decode(VoidResult.self, from: data1)
//VoidResult as expected

let errorInstanceDecoded = try! JSONDecoder().decode(MyError.self, from: data2)
//MyError.general as expected

let voidInstanceDecodedFromError = try! JSONDecoder().decode(VoidResult.self, from: data2)
//VoidResult - NOT EXPECTED

let errorInstanceDecodedFromVoid = try! JSONDecoder().decode(ScreenError.self, from: data1)
//DecodingError.typeMismatch - Expected

So my suggestion is to add "uniqueness to NoReply (zoul's answer)):

struct VoidResult: Codable {
    var id = UUID()
}

let voidInstanceDecodedFromError = try! JSONDecoder().decode(VoidResult.self, from: data2)
//DecodingError.typeMismatch - Now its fine - as expected
protspace
  • 2,047
  • 1
  • 25
  • 30