wireform-bson
wireform-bson implements BSON, the binary document format used by MongoDB
on the wire and in storage. BSON extends JSON-like documents with typed
fields, binary subtypes, and MongoDB-specific types such as ObjectId and
Decimal128. Use this package when you talk to MongoDB drivers, parse
change streams, or exchange documents with services that speak BSON rather
than JSON.
Key features
Section titled “Key features”- Template Haskell deriving via
deriveBSONfor Haskell record types, withwireform-deriveannotations; Generic defaults (empty instances) work for simple cases - Full MongoDB element set including
ObjectId,Decimal128, JavaScript code, regex, timestamps, and MinKey/MaxKey - Binary subtypes for UUID, user-defined payloads, and other BSON binary conventions
- Dynamic values via the untyped
ValueADT for schema-less documents - Direct encoding for pre-sized buffer writes on hot paths
Basic usage
Section titled “Basic usage”Map a Haskell record to a BSON document with the Template Haskell deriver:
{-# LANGUAGE DeriveGeneric #-}{-# LANGUAGE TemplateHaskell #-}module UserDoc where
import BSON.Class (ToBSON, FromBSON, encodeBSON, decodeBSON)import BSON.Derive (deriveBSON)import GHC.Generics (Generic)import Data.ByteString (ByteString)import Data.Text (Text)
data User = User { userName :: !Text , userAge :: !Int , userId :: !ByteString } deriving stock (Show, Eq, Generic)
$(deriveBSON ''User)
insertBytes :: User -> ByteStringinsertBytes user = encodeBSON user
readUser :: ByteString -> Either String UserreadUser bs = decodeBSON bsFor simple cases with no wire-format customization, Generic defaults also
work: add deriving Generic and declare empty instance ToBSON User and
instance FromBSON User declarations.
When you need MongoDB-specific field types, model them with the Value
constructors and use the dynamic ADT, or wrap the wire shapes in newtypes
with custom instances:
import BSON.Value qualified as Bimport Data.Vector qualified as V
paymentDoc :: B.ValuepaymentDoc = B.Document $ V.fromList [ ("amount", B.Decimal128 amountBytes) , ("note", B.JavaScript "function() { return true; }") , ("tags", B.Regex "paid" "i") ]For documents whose shape is only known at runtime, work with the dynamic ADT:
import BSON.Value qualified as Bimport BSON.Encode (encode)import BSON.Decode (decode)import Data.Vector qualified as V
lookupName :: B.Value -> Maybe TextlookupName doc = case doc of B.Document fields -> case V.find ((== "name") . fst) (V.toList fields) of Just (_, B.String t) -> Just t _ -> Nothing _ -> NothingPerformance
Section titled “Performance”Encode/decode
Section titled “Encode/decode”| Operation | encode | decode | ratio |
|---|---|---|---|
| single Person | 309 ns | 399 ns | 1.29x |
| [Person] x 100 | 55343 ns | 23567 ns | 0.43x |
Last run 2026-06-27 11:35:54 UTC. ghc-9.8.4 on darwin-aarch64, criterion 1.6.5.
Single-record round-trips under a microsecond. Batch decode is faster than encode because the BSON wire format allows scanning without full materialization of nested documents.
The chart and table above are regenerated by wireform-stats from wireform-bson/bench-results/summary/bson-encode-decode.json — the same source the README chart is built from.
Notable modules
Section titled “Notable modules”| Module | Purpose |
|---|---|
BSON.Class | ToBSON / FromBSON, encodeBSON, decodeBSON |
BSON.Encode / BSON.Decode | Low-level wire encode and decode |
BSON.Value | Dynamic Value ADT and MongoDB-specific types (ObjectId, Decimal128, Regex, …) |
BSON.Derive | Template Haskell deriver with wireform-derive annotations |