Casting `[String: Any]` to `[String: any Sendable]`

I have a simple wrapper class around WCSession to allow for easier unit testing. I'm trying to update it to Swift 6 concurrency standards, but running into some issues. One of them is in the sendMessage function (docs here

It takes [String: Any] as a param, and returns them as the reply. Here's my code that calls this:

    @discardableResult
    public func sendMessage(_ message: [String: Any]) async throws -> [String: Any] {
        return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in
            wcSession.sendMessage(message) { response in
                continuation.resume(returning: response) // ERROR HERE
            } errorHandler: { error in
                continuation.resume(throwing: error)
            }
        }
    }

However, I get this error:

Sending 'response' risks causing data races; this is an error in the Swift 6 language mode

Which I think is because Any is not Sendable. I tried casting [String: Any] to [String: any Sendable] but then it says:

Conditional cast from '[String : Any]' to '[String : any Sendable]' always succeeds

Any ideas on how to get this to work?

Answered by DTS Engineer in 807396022

https://developer.apple.com/forums/thread/765375

A key goal of Swift concurrency is that the compiler can check that your concurrent code follows all the rules. For that to work properly, the compiler needs type information. Any time you erase the types, like working with Any values, things get tricky. This is problematic because many older APIs, those with an Objective-C heritage, work with Any types all the time.

In this specific case, the Any values in the dictionaries used by sendMessage(_:replyHandler:errorHandler:) are a bit of a lie. You can’t transport any value over this connection. The values must be property list serialisable. And that’s a key hint for how to approach problems like this.

The folks who call sendMessage(…) don’t send any data across the connection. They send specific requests and expect back specific responses. So I’d tackle this problem by making these requests and responses explicit. For example:

enum MyRequest {
    case a
    case b
    case c
    var dictionary: [String: Any] {
        … your code here …
    }
}

enum MyResponse {
    case x
    case y
    case z
    init?(dictionary: [String: Any]) {
        … your code here …
    }
}

@discardableResult
func sendMessageQ(_ request: MyRequest) async throws -> MyResponse {
    return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<MyResponse, Error>) in
        wcSession.sendMessage(request.dictionary) { responseDict in
            guard let response = MyResponse(dictionary: responseDict) else {
                continuation.resume(throwing: …)
                return
            }
            continuation.resume(returning: response) // ERROR HERE
        } errorHandler: { error in
            continuation.resume(throwing: error)
        }
    }
}

This has a couple of advantages:

  • It fixes your problem, because MyRequest and MyResponse are just enums, and hence sendable.

  • And the dictionary serialisation and deserialisation code never crosses an isolation boundary.

  • It centralises your parsing of network data. This is important because you can’t ‘trust’ data coming in from the network, and thus you want to check it as you receive it. Your current approach does this checking in many places — all the call site for your sendMessage(_:) routine — and that’s less than ideal.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

https://developer.apple.com/forums/thread/765375

A key goal of Swift concurrency is that the compiler can check that your concurrent code follows all the rules. For that to work properly, the compiler needs type information. Any time you erase the types, like working with Any values, things get tricky. This is problematic because many older APIs, those with an Objective-C heritage, work with Any types all the time.

In this specific case, the Any values in the dictionaries used by sendMessage(_:replyHandler:errorHandler:) are a bit of a lie. You can’t transport any value over this connection. The values must be property list serialisable. And that’s a key hint for how to approach problems like this.

The folks who call sendMessage(…) don’t send any data across the connection. They send specific requests and expect back specific responses. So I’d tackle this problem by making these requests and responses explicit. For example:

enum MyRequest {
    case a
    case b
    case c
    var dictionary: [String: Any] {
        … your code here …
    }
}

enum MyResponse {
    case x
    case y
    case z
    init?(dictionary: [String: Any]) {
        … your code here …
    }
}

@discardableResult
func sendMessageQ(_ request: MyRequest) async throws -> MyResponse {
    return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<MyResponse, Error>) in
        wcSession.sendMessage(request.dictionary) { responseDict in
            guard let response = MyResponse(dictionary: responseDict) else {
                continuation.resume(throwing: …)
                return
            }
            continuation.resume(returning: response) // ERROR HERE
        } errorHandler: { error in
            continuation.resume(throwing: error)
        }
    }
}

This has a couple of advantages:

  • It fixes your problem, because MyRequest and MyResponse are just enums, and hence sendable.

  • And the dictionary serialisation and deserialisation code never crosses an isolation boundary.

  • It centralises your parsing of network data. This is important because you can’t ‘trust’ data coming in from the network, and thus you want to check it as you receive it. Your current approach does this checking in many places — all the call site for your sendMessage(_:) routine — and that’s less than ideal.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@Quinn,

I'm missing something here.

init?(dictionary: [String: Any]) {
        fatalError()
    }

So why does this call not trigger fatalError ?

guard let response = MyResponse(dictionary: responseDict)

Ah, sorry about that, those fatalError() calls were placeholders. I’ve edited the post to make that clear.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Casting `[String: Any]` to `[String: any Sendable]`
 
 
Q