BSON Usage Examples
Using BSON in production generally consists of three kinds of tasks: defining root types that model the documents you want to store as BSON, defining reusable supporting types to model fields in other types, and actually encoding and decoding BSON documents to and from binary data.
Modeling Documents
To model a document, you must provide a CodingKey, an encode(to:) witness, and a init(bson:) witness.
The two conformances are theoretically independent, though you will almost always want to implement both at the same time. Below is an example of a model type with two fields, id and name.
struct Model:BSONDocumentEncodable, BSONDocumentDecodable
{
let id:Int64
let name:String
enum CodingKey:String, Sendable
{
case id = "_id"
case name = "N"
}
func encode(to bson:inout BSON.DocumentEncoder<CodingKey>)
{
bson[.id] = self.id
bson[.name] = self.name
}
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.id = try bson[.id].decode()
self.name = try bson[.name].decode()
}
}Examples.swift:3In the rest of these examples, we’ll omit the CodingKey definitions unless they are truly interesting.
Optionals
When encoding optional fields, the encoder stays the same, as BSON.DocumentEncoder.subscript(_:) will simply omit the field if the assigned value is nil.
In the initializer, you should chain the accessed field with the optional chaining operator (?) before calling decode(to:). If you omit this, the decoder will throw an error if the field is missing.
struct ModelWithOptional:BSONDocumentEncodable, BSONDocumentDecodable
{
let x:Int32?
func encode(to bson:inout BSON.DocumentEncoder<CodingKey>)
{
bson[.x] = self.x
}
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.x = try bson[.x]?.decode()
}
}Examples.swift:28Arrays
Native Swift arrays will encode themselves as BSON lists. Some applications find it convenient to elide list fields if they are empty, and this can be expressed concisely as shown below.
struct ModelWithList:BSONDocumentEncodable, BSONDocumentDecodable
{
let x:[String]
func encode(to bson:inout BSON.DocumentEncoder<CodingKey>)
{
bson[.x] = self.x.isEmpty ? nil : self.x
}
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.x = try bson[.x]?.decode() ?? []
}
}Examples.swift:51Sets and Dictionaries
For many applications, serializing Dictionary is problematic because its key-value pairs do not have a deterministic order. This is bad for caching. Set suffers from a similar problem. That said, both types are still round-trippable provided their elements are themselves round-trippable.
struct ModelWithCollections:BSONDocumentEncodable, BSONDocumentDecodable
{
let x:[BSON.Key: Int32]
let y:Set<Double>
func encode(to bson:inout BSON.DocumentEncoder<CodingKey>)
{
bson[.x] = self.x.isEmpty ? nil : self.x
bson[.y] = self.y.isEmpty ? nil : self.y
}
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.x = try bson[.x]?.decode() ?? [:]
self.y = try bson[.y]?.decode() ?? []
}
}Examples.swift:74The Dictionary conformance is only available when the dictionary’s key type conforms to BSON.Keyspace. This protocol refines RawRepresentable, and imposes the additional semantic requirement that the RawRepresentable.rawValue string must not contain null bytes.
Nested Documents
Any type that conforms to BSONEncodable and BSONDecodable can be used as a field in another type. Since BSONDocumentEncodable and BSONDocumentDecodable refine those respective protocols, this means any BSON model type can be used as a field in another BSON model type.
struct ModelWithNestedDocument:BSONDocumentEncodable, BSONDocumentDecodable
{
let x:Model
let y:Model?
let z:[Model]
func encode(to bson:inout BSON.DocumentEncoder<CodingKey>)
{
bson[.x] = self.x
bson[.y] = self.y
bson[.z] = self.z.isEmpty ? nil : self.z
}
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.x = try bson[.x].decode()
self.y = try bson[.y]?.decode()
self.z = try bson[.z]?.decode() ?? []
}
}Examples.swift:101Explicit Null Values
The distinction between a field that is missing and a field that is explicitly null is usually irrelevant, but if you need to distinguish between the two cases, supply the decoded type explicitly to decode(to:). This will fail with an error if the field is present, but contains a null value.
struct ModelWithNoExplicitNull:BSONDocumentEncodable, BSONDocumentDecodable
{
let x:String?
init(bson:BSON.DocumentDecoder<CodingKey>) throws
{
self.x = try bson[.x]?.decode(to: String.self)
}
}Examples.swift:130If you don’t supply the type explicitly, the Swift compiler will infer a default type of String?.Type because the result of the call is being assigned to an optional. Optional’s own conditional conformance to BSONDecodable maps null to Optional.none, which is what you usually want instead.
Primitive Types
The library provides conformances for a number of primitives.
| Primitive | Encodable? | Decodable? |
|---|---|---|
Bool | ✓ | ✓ |
Int32 | ✓ | ✓ |
Int64 | ✓ | ✓ |
Double | ✓ | ✓ |
String | ✓ | ✓ |
UnixMillisecond | ✓ | ✓ |
BSON.Decimal128 | ✓ | ✓ |
BSON.Identifier | ✓ | ✓ |
BSON.Timestamp | ✓ | ✓ |
BSON.Regex | ✓ | ✓ |
BSON.Max | ✓ | ✓ |
BSON.Min | ✓ | ✓ |
BSON.Null | ✓ | ✓ |
Prefer UnixMillisecond over BSON.Timestamp when representing dates.
Integers and Floats
Although they are not all primitives, the library provides BSONDecodable conformances for all Swift integer types.
Integers of bit width less than 32 bits are round-trippable, but they will be represented as Int32 in the database, so there is no storage benefit to using them.
| Primitive | Encodable? | Decodable? |
|---|---|---|
Int | ✓ | ✓ |
Int8 | ✓ | ✓ |
Int16 | ✓ | ✓ |
UInt8 | ✓ | ✓ |
UInt16 | ✓ | ✓ |
UInt32 | ✓ | |
UInt64 | ✓ | |
UInt | ✓ | |
Float | ✓ |
It is generally not a good idea to use UInt32, UInt64, or UInt in BSON schema. Unsigned integers can be encoded losslessly as their respective signed integer types, but they will sort incorrectly in MongoDB.
Similarly, Float is decodable, but not round-trippable. This is an inherent characteristic of IEEE-754 floating-point numbers, one example of which is sNaN(0x1) which will decode to NaN(0x1) if converted to Double.
The library also provides overlays for the dimensional types from the UnixTime module.
| Primitive | Encodable? | Decodable? |
|---|---|---|
Milliseconds | ✓ | ✓ |
Seconds | ✓ | ✓ |
Minutes | ✓ | ✓ |
Strings and Characters
As a natural extension of the String primitive, the library provides error-handling conformances for Character and Unicode.Scalar.
| Primitive | Encodable? | Decodable? |
|---|---|---|
Substring | ✓ | ✓ |
Character | ✓ | ✓ |
Unicode.Scalar | ✓ | ✓ |
There is no performance benefit to decoding Substring instead of String, as both will involve copying the underlying storage. However, using Substring over String may save some applications a buffer copy on the encoding side.
Abstractions
Never is abstractly round-trippable, which is useful in generic contexts.
Swift’s native lazy sequences are conditionally encodable, which can help avoid unnecessary allocations in some situations.
| Primitive | Encodable? | Decodable? |
|---|---|---|
Never | ✓ | ✓ |
LazyMapSequence | ✓ | |
LazyFilterSequence | ✓ | |
LazyDropWhileSequence | ✓ | |
LazyPrefixWhileSequence | ✓ |
Custom Types
To encode and decode custom types, you must directly or indirectly implement the BSONEncodable.encode(to:) and BSONDecodable.init(bson:) requirements. The vast majority of custom types can obtain these witnesses via RawRepresentable or LosslessStringConvertible.
Delegation to Raw Representation
You can delegate to RawRepresentable whenever a type has a RawValue that is already BSONDecodable or BSONEncodable.
enum Continent:Int32, BSONEncodable, BSONDecodable
{
case africa
case antarctica
case asia
case australia
case europe
case northAmerica
case southAmerica
}Examples.swift:153Delegation to String Representation
You can delegate to LosslessStringConvertible to use a type’s String representation as its database representation.
Here’s an example of a type that stores a logical String and Int pair, and uses a formatted host:port string representation for encoding and decoding:
struct Host:LosslessStringConvertible,
BSONStringEncodable,
BSONStringDecodable
{
let name:String
let port:Int?
init?(_ string:String)
{
if let i:String.Index = string.firstIndex(of: ":"),
let port:Int = .init(string[string.index(after: i)...])
{
self.name = String.init(string[..<i])
self.port = port
}
else
{
self.name = string
self.port = nil
}
}
var description:String
{
self.port.map { "\(self.name):\($0)" } ?? self.name
}
}Examples.swift:165Unlike delegation to raw representation, delegation to string representation is not enabled by default. You must opt-in to it by conforming to the BSONStringDecodable and BSONStringEncodable protocols.
Converting to and from Raw Data
A complete BSON “file” is generally understood to consist of a single top-level container, usually a document.
Binding to a Document
The snippet below contains a full BSON document — including the header and trailing null byte — in the variable full. We usually omit the top-level wrapper when storing BSON on disk, and this is the portion the Document.init(bytes:) initializer expects, so we slice full before binding it to document.
let full:[UInt8] = [
0x09, 0x00, 0x00, 0x00, // Document header
0x08, 0x62, 0x00, 0x01, // Document body
0x00 // Trailing null byte
]
let bson:BSON.Document = .init(bytes: full[4 ..< 8])DocumentStructure.swift:31Keep in mind that binding to a document performs no parsing, since the whole point of BSON is to do as little parsing as possible, as late as possible.
Parsing a Document
To parse a document, pass it to BSONDocumentDecodable.init(bson:).
let decoded:BooleanContainer = try .init(bson: bson)DocumentStructure.swift:41Serializing a Document
To serialize a model type, pass it to BSON.Document.init(encoding:). You can then get the underlying ArraySlice of bytes from BSON.Document.bytes.
let encoded:BSON.Document = .init(encoding: decoded)
let data:ArraySlice<UInt8> = encoded.bytesDocumentStructure.swift:43