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.

Serialization Patterns.md

Lists and Sequences

The native Swift Array type is your best friend when working with sequences (See: Best Practices). However, there are rare situations where you may want to use the library’s list abstractions instead.

The serialization of sequences is less symmetrical than the serialization of single values, due to memory allocation concerns.

Encoding Sequences

The protocol for encoding sequences is BSONListEncodable. Its default implementations are available for any Sequence with an Element type that is BSONEncodable.

The principal benefit of BSONListEncodable is that it avoids encoding many temporary BSON entities or any intermediate arrays to hold those temporary BSON fragments.

Encoding Manually

Many applications will converge on Sequence-powered encoding, as this involves the least amount of total code since your Swift logic likely also wants to use sequences. However, it is also possible to encode BSON lists manually.

The interface for manual BSON.List encoding is BSON.ListEncoder. The BSONListEncodable.encode(to:) witness you provide receives an instance of this type.

Here’s an example of manual BSON list encoding, which expands every element of a Range into a list of BSON integers:

struct NumbersExpanded:BSONListEncodable
{
    let range:Range<Int32>

    func encode(to bson:inout BSON.ListEncoder)
    {
        for value:Int32 in self.range
        {
            bson[+] = value
        }
    }
}
Patterns.swift:3

The BSON.ListEncoder type is a powerful interface that also allows you to encode arbitrary nested lists and documents. The example code below encodes alternating nested lists and documents within a larger list:

enum SillyElementKey:String, Sendable
{
    case minusOne = "M"
    case plusOne = "P"
}

func encode(to bson:inout BSON.ListEncoder)
{
    for value:Int32 in self.range
    {
        bson
        {
            $0[+] = value - 1
            $0[+] = value + 1
        }
        bson(SillyElementKey.self)
        {
            $0[.minusOne] = value - 1
            $0[.plusOne] = value + 1
        }
    }
}
Patterns.swift:21

The JSON equivalent to the BSON it would produce would look something like this:

[
    [0, 2], {"M": 0, "P": 2},
    [1, 3], {"M": 1, "P": 3},
    [2, 4], {"M": 2, "P": 4},
    [3, 5], {"M": 3, "P": 5},
]

Decoding Lists

The interface for decoding BSON lists is BSON.ListDecoder. You receive an instance of this type by conforming to the BSONListDecodable protocol.

The BSON.ListDecoder type’s job is to provide a sequential iterator for decoding list elements. Thus, it allocates no internal structures to index the positions of the list elements within the underlying BSON buffer.

You would usually use BSONListDecodable when you expect a list of a fixed size, or a length that is a multiple of some fixed stride. Here’s an example of a type FirstAndLastName that round-trips a pair of strings:

struct FirstAndLastName:BSONListEncodable, BSONListDecodable
{
    let firstName:String
    let lastName:String

    func encode(to bson:inout BSON.ListEncoder)
    {
        bson[+] = self.firstName
        bson[+] = self.lastName
    }

    init(bson:consuming BSON.ListDecoder) throws
    {
        self.firstName = try bson[+].decode()
        self.lastName = try bson[+].decode()
        //  Verify that there are no more elements in the list.
        try bson[+]?.decode(to: Never.self)
    }
}
Patterns.swift:46

It is idiomatic to optionally decode the end of the list to Never to ensure that the list is fully consumed. This is a safety feature that prevents you from accidentally ignoring extra trailing elements in the BSON list.

Best Practices

BSON lists are essentially BSON documents with anonymous keys. The BSONListEncodable and especially BSONListDecodable protocols are best suited for schema with positionally-significant list items. However, unlike JSON, encoding with anonymous keys saves no space relative to encoding with named keys, so it just results in schema that is more brittle and harder to evolve.

Some kinds of data, like arrays of RGB colors, are logically well-modeled as lists, but can be represented far more efficiently as packed binary data.

Prefer Array

There are few compelling reasons to use BSONListEncodable or BSONListDecodable in your schema design.

In virtually all remaining cases, Array should be your preferred abstraction for representing homogeneous sequences. Its BSONDecodable implementation is connected directly to the BSON parser, and can populate itself with no intermediate allocations.

Avoid Non-deterministic Sequences

Some Swift data structures (such as Set) do not have a deterministic order.

The swift-bson library provides BSONDecodable and BSONEncodable implementations for Set, which can save you an array allocation when performing one-way BSON decoding. Keep in mind though, that persisting Set is bad for cache performance, since the output BSON will be different every time.

See also

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

    Read More
  • 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
  • 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