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.

Examples.md

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:3

In 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:28

Arrays

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:51

Sets 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:74

The 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:101

Explicit 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:130

If 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.

PrimitiveEncodable?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.

PrimitiveEncodable?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.

PrimitiveEncodable?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.

PrimitiveEncodable?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.

PrimitiveEncodable?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:153

Delegation 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:165

Unlike 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:31

Keep 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:41

Serializing 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.bytes
DocumentStructure.swift:43

See also

  • BSON Decoding and Encoding, Explained

    This article walks through the process of implementing BSONDocumentDecodable and BSONDocumentEncodable conformances for a model type in detail, and explains the rationale behind library’s design decisions.

    Read More
  • Advanced Serialization Patterns

    Level up your BSON encoding skills by learning these composable serialiation patterns. Internalizing these techniques will help you write more efficient and maintainable database applications by avoiding temporary allocations and reusing generic library code.

    Read More
  • Textures and Coordinates

    BSON documents can embed arbitrary binary data. This is useful for storing trivial repeating values like RGB colors or 3D coordinates, and can save an enormous amount of keying overhead, though at the cost of making the data un-queryable, as the database will not understand your custom data format.

    Read More