IDs
Choose how the _id of each document is generated, compare ObjectIds, and add the id virtual that Mongoose had.
Every document has an _id. The schema decides what it stores and who generates it: the driver,
when the schema leaves it out, or the schema's own .default().
import { ObjectId } from 'mongodb'
import { mongozod } from 'mongozod'
import * as mz from 'mongozod/mzod'
import * as z from 'zod'
// Not declared: the driver generates an ObjectId.
const tagSchema = z.object({
name: z.string()
})
// An ObjectId that the schema generates.
const postSchema = z.object({
_id: mz.objectId()
.default(() => new ObjectId()),
title: z.string(),
authorId: mz.objectId()
})
// A UUID string that the schema generates.
const sessionSchema = z.object({
_id: z.uuidv4()
.default(() => crypto.randomUUID()),
userId: mz.objectId(),
expiresAt: mz.date()
})Leave it out
Without an _id in the schema, the driver adds an ObjectId to each document it inserts. Reads
type the _id as an ObjectId, filters take one, and insertOne returns it in insertedId and
in document:
const TagsModel = mongozod({
collection: 'tags',
db,
schema: tagSchema
})
const { document: tag } = await TagsModel.insertOne({ name: 'mongodb' })
// { _id: ObjectId('…'), name: 'mongodb' }What is built from the schema doesn't know about it, though:
mz.filter(tagSchema, { _id: ['$eq'] }) doesn't compile, and z.encode leaves the _id out, as a
z.object does with any key it doesn't declare. Declare the _id when an endpoint takes it or
returns it.
An ObjectId from the schema
mz.objectId() stores an ObjectId and also takes its 24-character hex string, which is what a
JSON client sends. Its .default() generates one for each insert, so inserts don't ask for it:
const PostsModel = mongozod({
collection: 'posts',
db,
schema: postSchema
})
const { document: post } = await PostsModel.insertOne({
title: 'Hello, MongoDB',
authorId: '64f1c2a9e4b0a1b2c3d4e5f6'
})
// { _id: ObjectId('…'), title: 'Hello, MongoDB', authorId: ObjectId('64f1c2a9e4b0a1b2c3d4e5f6') }Without the .default(), every insert has to give the _id. An upsert that inserts a document
also gets it from the default, and a replacement keeps the _id of the document it replaces. See
BSON types for what mz.objectId() takes.
Import ObjectId from mongodb on the server. A schema shared with a browser can import it from
bson, the package mongozod/mzod builds its values with, so it doesn't load the driver.
A UUID
z.uuidv4() stores the _id as a string of 36 characters, and crypto.randomUUID(), the Web
Crypto global of Node.js and browsers, generates it:
const SessionsModel = mongozod({
collection: 'sessions',
db,
schema: sessionSchema
})
const { insertedId } = await SessionsModel.insertOne({
userId: post.authorId,
expiresAt: new Date(Date.now() + 86_400_000)
})
// insertedId: string, such as '0f6b4c4e-6a8e-4f0e-9a52-2b7d1f0b8c3a'To store it as BSON binary, in 16 bytes, use mz.uuid(), also in
BSON types. Define the _id field once and reuse it in each schema, so
every collection generates it the same way.
Compare ObjectIds with .equals()
Two ObjectIds with the same value are still two objects, so === is false. Compare them with
.equals(), which also takes the hex string:
const authorId = mz.objectId().parse('64f1c2a9e4b0a1b2c3d4e5f6')
const posts = await PostsModel.find({ authorId })
for (const found of posts) {
found.authorId.equals(authorId) // true
found.authorId.equals('64f1c2a9e4b0a1b2c3d4e5f6') // true
found.authorId === authorId // false: two objects
}Don't check them with instanceof either: in an ES module app, found.authorId and authorId
come from two builds of bson, so one of them fails it. .equals() and ObjectId.isValid() work
with both, and so do filters and writes. See BSON types for
why.
The id virtual
Mongoose adds an id getter to every schema, with the _id as a string
(Mongoose docs). MongoZod adds nothing on its own:
declare it as a virtual that picks the _id:
import { ObjectId } from 'mongodb'
import { mongozod } from 'mongozod'
import * as mz from 'mongozod/mzod'
import * as z from 'zod'
const userSchema = z.object({
_id: mz.objectId()
.default(() => new ObjectId()),
name: z.string()
})
const UsersModel = mongozod({
collection: 'users',
db,
schema: userSchema,
virtuals: {
id: userSchema.pick({ _id: true })
.transform((user) => user._id.toHexString())
}
})
const users = await UsersModel.find()
// [{ _id: ObjectId('6acd…'), name: 'Ana', id: '6acd…' }]
const ids = await UsersModel.find({}, { projection: { _id: 0, id: 1 } })
// [{ id: '6acd…' }]Reads that return documents compute it, and it isn't stored, so a filter uses the _id. When the
schema doesn't declare the _id, the virtual declares it in its own object:
z.object({ _id: mz.objectId() }).transform(…).
You may not need it for a JSON response: JSON.stringify already turns each ObjectId into its hex
string.
Common mistakes
- Comparing with
===orinstanceof:===isfalsefor twoObjectIds with the same value, and in an ES module appinstanceoffails for the ones of the other build ofbson. Use.equals(). - Filtering by the hex string:
{ _id: '64f1…' }on anObjectIdfield doesn't compile, and without types it matches nothing, because MongoZod doesn't cast filters. Parse the string withmz.objectId(), or take the filter throughmz.filter. mz.objectId()without.default()on the_id: every insert has to pass the_id, and one that doesn't fails with aZodError(received undefined). Add.default(() => new ObjectId()), or leave the_idout of the schema.
Last updated on
MongoZod is an independent open-source project. It is not affiliated with, endorsed by or sponsored by MongoDB, Inc. MongoDB® is a registered trademark of MongoDB, Inc.