How to fix Non-sendable type 'Any Encodable' error in Swift 6

Hello guys, I have a below enum in our network module,

public enum NetworkRequestBody {
  case json(Encodable)
  case form([String: String])
  case rawData(Data, String)
}

Having .json(Encodable) allows us to send any Codable which is very convenient but now after making it Sendable we are getting this warning,

Associated value 'json' of 'Sendable'-conforming enum 'NetworkRequestBody' has non-sendable type 'any Encodable'; this is an error in the Swift 6 language mode

I have not found a good solution on how to get around this warning. Any help is highly appreciated.

You might be able to do Encodable & Sendable. If not you can make a typealias of those and use that typealias instead of Encodable

3 Likes

Thanks it did help :tada:

I want to share an alternative design so you can compare/contrast it with the design you have now.

To avoid existentials (any ...) you can use generics to make your code more strongly typed. What you're doing here is erasing not just the type of the encoded value but also the type of the request (although it can be recovered by a switch statement) so that code using NetworkRequest can be "generic" and work with any type of request. The other option is to make code that wants to work with any network request literally generic (the angle brackets type).

First you would change NetworkRequest to a protocol:

protocol NetworkRequest: Sendable {}

Then declare a conforming type for each of those cases:

struct JsonRequest<T: Encodable & Sendable>: NetworkRequest {
  let value: T
}

struct FormRequest: NetworkRequest {
  let form: [String: String]
}

struct RawRequest: NetworkRequest {
  let data: Data
  let theString: String
}

(I didn't know what to call theString because I'm not sure what the second associated value in rawData is)

Then code that uses a request, like this:

func send(request: NetworkRequest) -> NetworkResponse {
  ...
}

Becomes this:

func send<R: NetworkRequest>(request: R) -> NetworkResponse {
  ...
}

Or more tersely:

func send(request: some NetworkRequest) -> NetworkResponse {
  ...
}

Any object that stores a NetworkRequest can become generic itself, or store it as an any NetworkRequest.

The question now is: what does generic code do with a request? Currently you're switching on an enum to deal with each case. You might be switching to get the Data payload to assign to the body of a URLRequest. Anywhere where you switch on the enum, you first extract it into a function (if needed) and then add that function as a requirement on the protocol, and move each case body into the implementation of the corresponding concrete type. This:

extension NetworkRequest {
  var serialized: Data {
    get throws {
      switch self {
        case let .json(value): try JSONEncoder().encode(value)
        case let .form(form): try encode(form: form)
        case let .rawData(data, _): data
    }
  }
}

Becomes this:

protocol NetworkRequest: Sendable {
  var serialized: Data { get throws }
}

struct JsonRequest<T: Encodable & Sendable>: NetworkRequest {
  let value: T

  var serialized: Data {
    get throws {
      try JSONEncoder().encode(value)
    }
  }
}

struct FormRequest: NetworkRequest {
  let form: [String: String]

  var serialized: Data {
    get throws {
      try encode(form: form)
    }
  }
}

struct RawRequest: NetworkRequest {
  let data: Data
  let theString: String

  var serialized: Data {
    get throws {
      data
    }
  }
}

If you still want to be able to exhaustively switch on the three cases in generic code, you can write a type eraser as an enum. You'd also need a parent protocol to erase the associated type of the json case:

protocol NetworkRequest: Sendable {
  ...

  var erased: AnyNetworkRequest { get }
}

protocol JsonRequestProtocol<T>: NetworkRequest {
  associatedtype T: Encodable & Sendable

  var value: T { get }
}

struct JsonRequest<T: Encodable & Sendable>: JsonRequestProtocol {
  ...

  var erased: AnyNetworkRequest { .json(self) }
}

struct FormRequest: NetworkRequest {
  ...

  var erased: AnyNetworkRequest { .form(self) }
}

struct RawRequest: NetworkRequest {
  ...

  var erased: AnyNetworkRequest { .raw(self) }
}

enum AnyNetworkRequest {
  case json(any JsonRequestProtocol)
  case form(FormRequest)
  case rawData(RawRequest)

  var unwrap: any NetworkRequest {
    switch self {
      case let .json(value): value
      case let .form(value): value
      case let .rawData(value): value
    }
  }
}

...

// How to use:
let aRequest: any NetworkRequest = getTheRequest()

switch aRequest.erased {
  case let .json(jsonRequest): ...
  case let .form(formRequest): ...
  case let .rawData(rawRequest): ...
}

That recreates the enum you have now, but you only have to use it if you absolutely need to erase the concrete type and then need to switch over the types later (this typically only happens if you need to store heterogenous requests in a collection). And you only need to switch over the concrete types if there's a capability you don't want to make a requirement of the protocol.

The advantage of this is you can constrain code to work only with some of those concrete types, and (if you add the JsonRequestProtocol) be able to fully recover the concrete types in generic code because they are bound to type parameters like R and T. It also means anyone can define their own new request type, which may be a good or bad thing depending on your goals. For example, if you have a type that represents an operation, you can make it generic, i.e. struct NetworkOperation<R: NetworkRequest>, and then do things like this:

extension NetworkOperation where R: JsonRequestProtocol {
  func describeThePayload() {
    print("The payload is a \"\(String(describing: R.T.self)\"")
  }
}

So the compiler is able to enforce that certain code only works with certain types of requests, and can provide full access to the constrained type, including any associated types.

If you find that you're hitting stuff like any Encodable can't conform to Encodable (some generic code isn't compatible with existentials) with the existentials approach, this is how you can avoid those problems. Generics are an alternative to existentials that can strengthen type safety by mostly (sometimes entirely) avoiding type erasure.

1 Like