Native MongoDB adapter — @ascendance-hub/sapphire-bson
The Mongo adapter converts a Sapphire IR (SapphireSchemaNode) into a MongoDB
collection validator — a { $jsonSchema: ... } document you hand to the
native driver when creating or modifying a collection. The database itself then
rejects documents that do not match.
This is the adapter for users on the plain mongodb driver — no Mongoose. If
you use Mongoose, reach for @ascendance-hub/sapphire-mongoose
instead.
Unofficial. A community adapter — not affiliated with, sponsored, or endorsed by MongoDB, Inc.
Install
npm install @ascendance-hub/sapphire-core @ascendance-hub/sapphire-bson
@ascendance-hub/sapphire-core is a peer dependency. mongodb is an optional
peer dependency — toBsonSchema emits a plain object and never imports the
driver, so you only need mongodb installed to actually create the collection.
Register the adapter
The adapter is not auto-registered. Call registerAdapter once in your
application entry point:
import { Sapphire, registerAdapter } from '@ascendance-hub/sapphire-core'
import { toBsonSchema } from '@ascendance-hub/sapphire-bson'
registerAdapter('bson', toBsonSchema)
export const a = new Sapphire({ defaultAdapter: 'bson' })
registerAdapter is process-global. The Mongoose adapter registers under the
separate name 'mongoose', so both can coexist in one process.
Quickstart
import { MongoClient } from 'mongodb'
import { toBsonSchema } from '@ascendance-hub/sapphire-bson'
import { a } from './sapphire'
const User = a.object({
name: a.string().min(1),
email: a.string().email(),
age: a.number().int().min(0).optional(),
})
const validator = toBsonSchema(User.toSchema())
// → { $jsonSchema: { bsonType: 'object', required: [...], properties: {...} } }
const client = new MongoClient(process.env.MONGO_URL!)
await client.connect()
const db = client.db('app')
await db.createCollection('users', { validator })
// or, on an existing collection:
await db.command({ collMod: 'users', validator })
field.getSchema('bson') is sugar for toBsonSchema(field.toSchema()) once
the adapter is registered.
IR → $jsonSchema mapping
toBsonSchema walks the IR and emits MongoDB’s flavor of JSON Schema —
bsonType instead of type, BSON type names, everything inlined (no $ref).
IR kind | $jsonSchema output | Notes |
|---|---|---|
string | { bsonType: 'string' } | minLength/maxLength/length direct. regex/startsWith/endsWith → pattern (multiple → allOf). format: email/uuid → pattern. format: url is dropped (see Limitations). |
number | { bsonType: 'number' } or { bsonType: 'int' } | .int() → bsonType: 'int'; otherwise 'number' (matches int/long/double/decimal). min/max/multipleOf map directly. Exclusive bounds (.gt()/.lt()) use JSON Schema draft-4 form — minimum/maximum plus a boolean exclusiveMinimum/exclusiveMaximum. |
boolean | { bsonType: 'bool' } | |
date | { bsonType: 'date' } | |
object | { bsonType: 'object', properties, required } | required lists every required key; omitted when empty. Named objects (.name(...)) are inlined — there is no $ref. |
array | { bsonType: 'array', items } | minItems/maxItems/length direct. |
tuple | { bsonType: 'array', items: [...], additionalItems: false } | items is an array of per-position schemas; minItems/maxItems pinned to the tuple length. |
union | { anyOf: [...] } | Requires MongoDB 5.0+ (when $jsonSchema gained anyOf). |
literal | { enum: [value] } | |
enum | { enum: [...values] } | |
record | { bsonType: 'object', additionalProperties: <values schema> } | |
ref | { bsonType: 'objectId' } | A reference is stored as an ObjectId — server-side validators are self-contained, so there is no $ref. |
Nullable
a.string().nullable() lifts the type into a bsonType array —
{ bsonType: ['string', 'null'] }. A nullable union/literal/enum (which
has no plain bsonType) wraps in anyOf with an explicit { bsonType: 'null' }
branch instead.
description
describe(text) becomes the $jsonSchema description keyword.
_id
There is no special handling — an _id is just another property:
- Declare an
_idfield and it emits like any other property (and lands inrequiredif required):a.object({ _id: a.string(), ... }). - Declare no
_idand the validator says nothing about it — MongoDB injects anObjectId_idserver-side as usual. - An
_idofa.ref('User')emits{ bsonType: 'objectId' }.
.adapter('bson', opts) escape hatch
Values from .adapter('bson', { ... }) are read from node.meta.bson and
merged into the emitted node, with a blacklist of keys the adapter computes
itself:
const META_BLACKLIST = new Set(['bsonType', 'type', 'required', 'enum', 'properties', 'items'])
[!WARNING] MongoDB rejects unknown
$jsonSchemakeywords.db.createCollectionthrows if the validator contains a keyword it does not recognize. Only pass keys that are valid$jsonSchemakeywords (title,minProperties,maxProperties,patternProperties, …) through the escape hatch.
BsonValidatorOptions
The second argument to toBsonSchema(node, options?):
| Option | Default | Effect |
|---|---|---|
additionalProperties | (omitted) | When set, emits additionalProperties on every object schema. Set false for a strict, closed-shape validator — the root object still permits MongoDB’s auto-injected _id. |
toBsonSchema(User.toSchema(), { additionalProperties: false })
Typed documents — BsonDoc
BsonDoc<F> is a type-only helper for the native driver’s
Collection<TSchema>:
import type { Collection } from 'mongodb'
import type { BsonDoc } from '@ascendance-hub/sapphire-bson'
const users: Collection<BsonDoc<typeof User>> = db.collection('users')
It is a thin alias over core’s Infer — the document shape is a purely
type-level concern, so there is no runtime helper.
Limitations
[!WARNING] A collection validator only validates. It does not transform input or fill defaults.
transforms(trim/toLowerCase/toUpperCase),default(v),coerce(), and the numericfinite/safechecks have no$jsonSchemaequivalent and are not emitted. Run them client-side viaparse()/safeParse()before inserting.
[!WARNING]
format('url')is not enforced server-side. There is no exported URL regex; email and uuid becomepattern, buturlis dropped from the validator. Validate URLs client-side viaparse().
[!WARNING]
unique/indexare not part of the validator. A$jsonSchemavalidator cannot declare indexes — create them withdb.collection.createIndex(...).
[!WARNING]
unionneeds MongoDB 5.0+.anyOfin$jsonSchemais unavailable on MongoDB 4.x.
Related
- Mongoose adapter — the Mongoose-based sibling package.
- Fields and modifiers — what each IR kind models.
- Refs and relations — ref lifecycle across adapters.
- Escape hatch — universal
.adapter(name, opts)contract. - Recipes → One schema, many adapters.