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.
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:3The 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:21The 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:46It 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.