Swift driver - issue encoding BSON array

I’m using MongoSwiftSync, and am attempting to do a simple $in query.

For this query:

let query: BSONDocument = [
  "someID": [
    "$in": [1,2,3]
  ]
]

This compiles fine. If I pass an [Int] into a function, and then reference it:

let query: BSONDocument = [
  "someID": [
    "$in": passedInArray
  ]
]

then I get an error:

error: cannot convert value of type ‘[Int]’ to expected dictionary value type ‘BSON’

If I check the type of the initial array and passedInArray, they both come back as Array. What am I doing wrong with the passedInArray? The driver isn’t happy about it.

Thanks.

1 Like

Hi @Mark_Windrim, welcome to the forums and thanks for reaching out!

The short answer, and what you need to get your code to compile, is to both declare that passedInArray is an [BSON] when you create it, and explicitly state the corresponding BSON enum case that passedInArray corresponds to:

let passedInArray: [BSON] = [1, 2, 3]

let query: BSONDocument = [
  "someID": [
    "$in": .array(passedInArray)
  ]
]

The long answer and why you need this is slightly tricky, but I will do my best to explain. It involves Swift’s type inference capabilities and ExpressibleBy protocols.

The BSON library has both a BSONDocument type which is essentially an ordered map of strings to BSON values, and a BSON type, which is an enum with associated values, where each case corresponds to a different BSON type.

The BSONDocument type conforms to the ExpressibleByDictionaryLiteral protocol, where the dictionary is a [String: BSON].

The BSON type conforms to a number of ExpressibleBy protocols:

  • ExpressibleByDictionaryLiteral with a [String: BSON]: when used, this initializes a new BSON.document
  • ExpressibleByArrayLiteral with a [BSON], iinitializes a new BSON.array
  • ExpressibleByIntegerLiteral, initializes a new BSON.int32 or BSON.int64, depending on what the width of Int on the platform you’re using is
  • ExpressibleByBooleanLiteral - initializes a new BSON.bool

For example, one could do something like

let d: BSON = ["a": 1]  // BSON.document(["a": 1])

Which would result in an instance of the BSON enum with case .document wrapping a BSONDocument created from ["a": 1].

Or:

let b: BSON = true // BSON.bool(true)

So in your first example, since you’ve added the BSONDocument type annotation, the compiler infers that "someID" is a String and that

[
    "$in": [1,2,3]
]

is a BSON, and since it is a dictionary literal it is inferred to be a [String: BSON].

Thus the compiler infers that $in is a String. Since [1, 2, 3] is an array literal, the compiler infers it to be an [BSON], and infers the individual elements 1, 2, 3, which are integer literals, to be BSONs as well.

Written without the help of the ExpressibleBy protocol implementations for BSON, your first document would look like:

let query: BSONDocument = [
  "someID": BSON.document([
    "$in": BSON.array([BSON.int64(1), BSON.int64(2), BSON.int64(3)])
  ])
]

Fortunately, you do not need to include the explicit enum cases for all of those.

Now, visiting your second example: the problem is that, since passedInArray is not an array literal and is just a plain old array, the corresponding BSON type, BSON.array, cannot be automatically instantiated from it. Thus, you need the .array(...) around it (the compiler can infer the BSON prefix), and when initializing it you need to tell the compiler that it’s an [BSON] and not an [Int].

You could also convert an [Int] to an [BSON] like: myArray.map { BSON.int64($0) }.

Hopefully that is helpful, and let me know if you have further questions! In short, when writing out a document, if you are using a literal you do not need to state which BSON enum case it corresponds to, but if you are using a variable, you do.

Relevant links:
Documentation for BSONDocument: https://mongodb.github.io/mongo-swift-driver/docs/current/MongoSwift/Structs/BSONDocument.html
Documentation for BSON type: https://mongodb.github.io/mongo-swift-driver/docs/current/MongoSwift/Enums/BSON.html
Blog post on the ExpressibleBy protocols: https://swiftrocks.com/swift-expressibleby-protocols-how-they-work-internally-in-the-compiler

1 Like

Hi,

Wow. Thank you for such a detailed response. I was able to get things to work as long as I defined the array within the function, but if I pass it into the function, then I still run into issues. ie:

func someFunction( incomingArray: [Int32]) throws -> MongoCursor<...>

It is the incomingArray above that I can’t get into BSON.

let bsonArray: [BSON] = incomingArray

results in:

error: cannot assign value of type '[Int32]' to type '[BSON]'

and if I do:

let bsonArray: [BSON] = .array(incomingArray) 

results in:

error: type '[BSON]' has no member 'array'

If I define the array specifically [1,2,3], then everything works as expected.

1 Like

I think you need to convert the individual array elements to BSONs as well. You could do this via map, like:

let bsonArray = incomingArray.map { BSON.int32($0) } // this gives you an [BSON]
let bson = BSON.array(bsonArray) // this gives you a BSON.array

And then use bson in your document.

Let me know if that works.

2 Likes

That did the trick! Thanks. I really appreciate all your help with this. Your response was very detailed.

Mark

2 Likes

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.