Skip to content

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.

  • Template Haskell deriving via deriveBSON for Haskell record types, with wireform-derive annotations; 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 Value ADT for schema-less documents
  • Direct encoding for pre-sized buffer writes on hot paths

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 -> ByteString
insertBytes user = encodeBSON user
readUser :: ByteString -> Either String User
readUser bs = decodeBSON bs

For 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 B
import Data.Vector qualified as V
paymentDoc :: B.Value
paymentDoc =
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 B
import BSON.Encode (encode)
import BSON.Decode (decode)
import Data.Vector qualified as V
lookupName :: B.Value -> Maybe Text
lookupName doc =
case doc of
B.Document fields ->
case V.find ((== "name") . fst) (V.toList fields) of
Just (_, B.String t) -> Just t
_ -> Nothing
_ -> Nothing
wireform-bson encode + decode (Person record) wireform-bson encode + decode (Person record) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 25000 50000 75000 100000 309 399 55343 23567 single Person [Person] x 100 encode decode wireform-bson encode + decode (Person record) lower is better · ns · ghc-9.8.4 on darwin-aarch64, criterion 1.6.5 0 25000 50000 75000 100000 309 399 55343 23567 single Person [Person] x 100 encode decode
Operationencodedecoderatio
single Person309 ns399 ns1.29x
[Person] x 10055343 ns23567 ns0.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.

ModulePurpose
BSON.ClassToBSON / FromBSON, encodeBSON, decodeBSON
BSON.Encode / BSON.DecodeLow-level wire encode and decode
BSON.ValueDynamic Value ADT and MongoDB-specific types (ObjectId, Decimal128, Regex, …)
BSON.DeriveTemplate Haskell deriver with wireform-derive annotations