npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nullix/zod-mongoose

v3.6.0

Published

Create Mongoose schemas using Zod v4.

Readme

Release Coverage NPM Version NPM Downloads install size Gzip Size TypeScript Node.js Version License

@nullix/zod-mongoose

A library which allows to author mongoose ("a MongoDB object modeling tool") schemas using zod ("a TypeScript-first schema declaration and validation library").

Purpose

Declaring mongoose schemas in TypeScript environment has always been tricky in terms of getting the most out of type safety:

  • You either have to first declare an interface representing a document in MongoDB and then create a schema corresponding to that interface (you get no type safety at all - even the offical mongoose documentation says that "you as the developer are responsible for ensuring that your document interface lines up with your Mongoose schema")
  • Or reverse things by using mongoose.InferSchemaType<typeof schema> which is far from ideal (impossible to narrow types, doesn't support TS enums, doesn't know about virtuals, has problems with fields named type, ...)
  • Finally, you can use typegoose which is based on legacy decorators proposal and generally poorly infers types.

This library aims to solve many of the aforementioned problems utilizing zod as a schema authoring tool.

1. Install

pnpm add @nullix/zod-mongoose

with dependencies:

pnpm add @nullix/zod-mongoose zod/v4 mongoose

Documentation

See the full documentation for detailed usage examples and API reference.

Validation and conversion

Built-in checks such as .min() and .regex() become Mongoose validators; string setters such as .trim() normalize on assignment. Field and object refinements also run during document validation. See the conversion reference for the complete mapping.

Zod v4 & Mongoose 9 Support

This package is now optimized for Zod v4 and Mongoose 9.

Key features:

  • Zod v4 registry: Securely store Mongoose-specific metadata alongside Zod schemas using native Zod v4 registry.
  • Output-aware conversion: Pipes, preprocessors, and codecs use their output schema. Bare transforms use Mixed; parse input before storage. Optional and nullable wrappers retain their underlying types.
  • Unions: Primitive Zod unions (string, number, boolean, date, bigint) are mapped to Mongoose Schema.Types.Union. Object unions are automatically merged into a single schema object where all properties are optional, providing a flexible way to handle polymorphic data. Other complex unions fallback to Mixed.
  • Native BigInt: Maps Zod bigint to native Mongoose BigInt (or Number as fallback). If you need Mongoose Long (64-bit integer), you can specify it via withMongoose(z.bigint(), { type: 'Long' }).
  • Specialized Types: Direct support for Buffer and ObjectId via zObjectId() and zBuffer() helpers.
  • Composite IDs: Full support for object-based _id fields via { includeId: true } metadata.
  • Isomorphic Support: The package treats mongoose as an optional peer dependency. Core Zod schema definition and metadata helpers (withMongoose, zObjectId, etc.) are safe to use in the browser without installing mongoose. Specialized types like ObjectId and Buffer automatically fall back to browser-compatible representations (strings and Uint8Arrays) while preserving Mongoose metadata for the backend.
  • Automatic Browser Detection: Utilizing package.json exports, the library automatically serves a frontend-optimized bundle when imported in browser environments, eliminating the need for manual configuration.
  • Nuxt 4 Ready: Use the core helpers in Nuxt and Nitro; conditional exports select the browser-safe client implementation automatically.
  • Hookable: Extensible conversion process using unjs/hookable. Developers can hook into 15+ points (e.g., schema:object:before, schema:union:before).
  • Populated Helper: PopulatedSchema<T> utility for perfect TypeScript inference of populated documents.
  • Mongoose Inference: InferDocument<T> includes the generated _id; InferInput<T> describes incoming Zod data; InferQueryFilter<T> derives type-safe MongoDB query filters.
  • Strict Model: toStrictModel<T> for advanced, fluent type-safe population (see Strict Model documentation).

zPoint() and zPolygon() provide GeoJSON values with native coordinate storage and validation. See the helper reference.

Values at the storage boundary

  • Arrays: use .default([]) when omission should produce an empty list. Otherwise required arrays reject omission and optional arrays remain omitted.
  • Transforms: parse input before constructing or assigning a document. Add .pipe(outputSchema) for automatic storage typing and output validation. Document validation does not rerun input callbacks; bare transforms use Mixed. See Validate Documents.
  • Casting: direct construction retains Mongoose casting. Parse with Zod first when raw input must satisfy Zod's rules before casting.

Unknown-key modes also apply to nested subschemas and arrays. A typed catchall validates dynamic values on document.validate() and save(); query updates do not run that document hook. For nested strict, loose, and catchall objects, use a subschema rather than {schema: false}. Mongoose-generated IDs, version keys, and timestamps are excluded from the package's Zod document validation unless the Zod object declares them. See the object modes guide for examples and option overrides.

Installation

Install the package:

pnpm add @nullix/zod-mongoose

Peer Dependencies

This package requires zod (^4.x). Note that zod imports should be from zod/v4 to ensure compatibility.

mongoose (^8.x) is an optional peer dependency. It is only required on the backend when calling toMongooseSchema(). You do not need to install mongoose on the frontend.

Usage

Basic Conversion

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema } from '@nullix/zod-mongoose';

const zodSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number().optional(),
});

// Basic conversion
const mongooseSchema = toMongooseSchema(zodSchema);

Plugins

You can pass Mongoose plugins directly to toMongooseSchema. This is the recommended way to extend your schemas with third-party or custom plugins.

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema } from '@nullix/zod-mongoose';
import mongooseLeanVirtuals from 'mongoose-lean-virtuals';

const zodSchema = z.object({
  title: z.string(),
});

// Apply plugins during conversion
const mongooseSchema = toMongooseSchema(zodSchema, {
  plugins: [mongooseLeanVirtuals]
});

// The resulting schema now has the plugin applied
const Post = mongoose.model('Post', mongooseSchema);
const doc = await Post.findOne().lean({ virtuals: true });

Hook: schema:created

For more advanced extensions, use the schema:created hook to modify the mongoose.Schema instance immediately after it is created.

import { hooks } from '@nullix/zod-mongoose';

hooks.hook('schema:created', ({ schema, zodSchema }) => {
  // Add a virtual field to all schemas that have a 'title'
  if ('title' in zodSchema.shape) {
    schema.virtual('slug').get(function() {
      return this.title.toLowerCase().replace(/ /g, '-');
    });
  }
});

Adding Mongoose Metadata

Use withMongoose to add Mongoose-specific field options like unique, index, or required.

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema, withMongoose } from '@nullix/zod-mongoose';

const zodSchema = z.object({
  username: withMongoose(z.string(), { 
    unique: true, 
    index: true,
    lowercase: true 
  }),
  // Default values set with zod's .default() are respected
  roles: z.array(z.string()).default(['user']),
});

const mongooseSchema = toMongooseSchema(zodSchema);

Timestamps

The genTimestampsSchema helper simplifies creating Mongoose-compatible Zod schemas with timestamp support.

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema, genTimestampsSchema, withMongoose } from '@nullix/zod-mongoose';

const userSchema = withMongoose(
  z.object({
    name: z.string(),
    ...genTimestampsSchema(),
  }),
  { timestamps: true }
);

const mongooseSchema = toMongooseSchema(userSchema);
// Mongoose schema will have { timestamps: true } automatically.

Discriminators

1. Native Mongoose Discriminators (Recommended)

When using z.discriminatedUnion(), the library automatically maps it to native Mongoose discriminators. This provides the most robust way to handle polymorphic data in Mongoose, including proper indexing and query support.

Top-level discriminated unions require modelName or collection. Mongoose registers discriminator model names globally, so the model name prevents collisions with standalone models. It affects only the Mongoose model name; the discriminator value stored in MongoDB remains the Zod literal value.

Common fields (fields present in all union branches) are automatically extracted and moved to the base schema.

import mongoose from 'mongoose';
import { z } from 'zod/v4';
import { toMongooseSchema, zObjectId } from '@nullix/zod-mongoose';

const ClickedEventZodSchema = z.object({
  type: z.literal('clicked'),
  timestamp: z.date(),
  target: z.string(),
});

const ViewedEventZodSchema = z.object({
  type: z.literal('viewed'),
  timestamp: z.date(),
  postId: zObjectId(),
});

const ActivityZodSchema = z.discriminatedUnion('type', [
  ClickedEventZodSchema,
  ViewedEventZodSchema,
]);

const ActivitySchema = toMongooseSchema(ActivityZodSchema, {
  modelName: 'Activity',
});
// Mongoose will create a base schema with 'timestamp' field
// and two discriminators ('clicked', 'viewed') for the other fields.

const ActivityModel = mongoose.model('Activity', ActivitySchema);

The discriminator key is type; its values (clicked and viewed) are stored in MongoDB. The base model name is Activity, while the generated Mongoose discriminator model names are Activity_clicked and Activity_viewed. zod-mongoose uses ${modelName}_${discriminatorValue} as the model name and passes the original value through { value: discriminatorValue }. If modelName is omitted, the name is derived from collection by singularizing it. Omitting both for a top-level discriminated union throws a helpful error.

Accessing discriminator models

toMongooseSchema() returns the base mongoose.Schema, not a model. After compiling it into a model, Mongoose exposes the compiled discriminator models through BaseModel.discriminators:

const ActivityModel = mongoose.model('Activity', ActivitySchema);
const ClickedEventModel = ActivityModel.discriminators?.Activity_clicked as
  mongoose.Model<z.infer<typeof ClickedEventZodSchema>>;

const event = await ClickedEventModel.findById(id);

The discriminator model uses the same collection and automatically filters on type: 'clicked'. The registered model name is Activity_clicked; the stored discriminator value remains clicked.

2. Manual Discriminators

If you prefer to define discriminators manually on a base model, you can define the discriminatorKey in the base schema's metadata.

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema, withMongoose } from '@nullix/zod-mongoose';

const baseSchema = withMongoose(
  z.object({
    name: z.string(),
    // Include the discriminator key in the Zod schema for type-safe access
    type: z.string().optional(),
  }),
  { discriminatorKey: 'type' }
);

const BaseModel = mongoose.model('Base', toMongooseSchema(baseSchema));

const carSchema = z.object({
  licensePlate: z.string(),
});

const CarModel = BaseModel.discriminator('Car', toMongooseSchema(carSchema));

Non-Inclusive Unions (XOR)

Use z.xor() for unions where exactly one option must match. This is mapped to Schema.Types.Mixed with a custom Zod validator that enforces mutual exclusivity at the database level.

const paymentSchema = z.xor([
  z.object({ type: z.literal('card'), cardNumber: z.string() }),
  z.object({ type: z.literal('bank'), accountNumber: z.string() }),
]);

const mongooseSchema = toMongooseSchema(z.object({ payment: paymentSchema }));

ObjectIds and _id Handling

By default, zObjectId() creates a required Zod field (unless .optional() is used) but omits the field from the generated Mongoose schema. This allows Mongoose to manage the automatic generation and internal lifecycle of the _id field without conflicts.

1. Standard Usage (Recommended)

For full document schemas, include the _id field using zObjectId().

const UserZodSchema = z.object({
  _id: zObjectId(), // Required for Zod validation, omitted from Mongoose schema
  name: z.string(),
});

2. Input/Create Schemas

When validating data for new records (e.g., in a POST request), you should use an Input Schema that omits the _id field. This ensures that the client cannot provide an ID that Mongoose should be generating.

const UserInputSchema = UserZodSchema.omit({ _id: true });
// Now UserInputSchema.parse({}) will succeed without an _id.

3. Explicitly Defining _id in Mongoose

If you need to explicitly define the _id field in the Mongoose schema (e.g., to add an index, a custom getter, or to disable auto-generation), use the includeId: true flag.

const CustomIdSchema = z.object({
  _id: zObjectId({ includeId: true, index: true }),
});

4. Composite IDs

Mongoose supports composite IDs (using an object as the _id). You can define this in Zod by using an object for the _id field and setting includeId: true in the metadata of the _id field (or the parent object).

const CompositeIdSchema = z.object({
  pk: z.string(),
  sk: z.string(),
});

const UserSchema = z.object({
  _id: withMongoose(CompositeIdSchema, { includeId: true }),
  name: z.string(),
});

const mongooseSchema = toMongooseSchema(UserSchema);
// Resulting Mongoose schema will have _id: { pk: String, sk: String }

Buffers and ObjectIds

For specialized Mongoose types, use the provided zObjectId() and zBuffer() helpers. They also accept optional metadata.

import { z } from '@nullix/zod-mongoose';
import { toMongooseSchema, zObjectId, zBuffer } from '@nullix/zod-mongoose';

const schema = z.object({
  avatar: zBuffer({ required: true }),
  ownerId: zObjectId({ index: true }),
  // Arrays are also supported
  tags: z.array(zObjectId()),
});

const mongooseSchema = toMongooseSchema(schema);

You can also use z.instanceof(Buffer) or z.instanceof(mongoose.Types.ObjectId) directly, which will be correctly inferred.

API Reference

toMongooseSchema(zodSchema, options?)

Converts a Zod schema to a Mongoose schema instance (new mongoose.Schema(...)).

  • zodSchema: A Zod object or any Zod type.
  • options: Optional Mongoose SchemaOptions.

extractMongooseDef(zodSchema)

Converts a Zod schema to a Mongoose schema definition object (the POJO used as the first argument to new mongoose.Schema(...)). This is useful if you want to manually create the Mongoose schema or combine it with other definitions.

  • zodSchema: A Zod object or any Zod type.

withMongoose(zodSchema, metadata)

Attaches Mongoose metadata to a Zod schema. For object schemas, use different placement for root and nested schema options:

| Where the object is used | Remove its generated _id and id virtual | | --- | --- | | Passed to toMongooseSchema() | withMongoose(object, { _id: false, id: false }) | | Nested inside another object | withMongoose(object, { schema: { _id: false, id: false } }) |

_id: false removes the identifier path; id: false only disables the string id virtual. Field options such as index and unique go directly in the metadata. See Nested Object IDs for the full behavior.

InferDocument<T> / InferInput<T> / InferQueryFilter<T>

Use InferDocument for values read from Mongoose, InferInput for data submitted to Zod, and InferQueryFilter for MongoDB query filters. InferDocument adds Mongoose's generated _id; InferInput is equivalent to z.input<T>, so it preserves coercion and preprocessing input types; InferQueryFilter derives the type-safe QueryFilter<InferDocument<T>> for queries and repository layers.

import { z } from 'zod/v4';
import type { InferDocument, InferInput, InferQueryFilter } from '@nullix/zod-mongoose';

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

type UserDocument = InferDocument<typeof UserSchema>;
type CreateUserInput = InferInput<typeof UserSchema>;
type UserFilter = InferQueryFilter<typeof UserSchema>;

const user: UserDocument = {
  _id: new mongoose.Types.ObjectId(),
  name: 'John Doe',
  age: 30,
};

const filter: UserFilter = {
  age: { $gte: 18 },
};

InferMongoose, OutputMongoose, and InputMongoose remain available in v3.1 for backwards compatibility. Migrate to InferDocument and InferInput; the legacy aliases will be removed in v4.

PopulatedSchema<T, K>

TypeScript utility type to extract the populated object type from a zRef union within a larger type. This is useful for typing Mongoose results after calling .populate().

  • T: The Zod-inferred type (e.g., z.infer<typeof PostSchema>).
  • K: The key(s) to populate (optional). If omitted, it will try to populate all zRef fields in the object.
import { PopulatedSchema } from '@nullix/zod-mongoose';

// Populate only the 'author' field
type PostWithAuthor = PopulatedSchema<z.infer<typeof PostSchema>, 'author'>;

// Populate all possible fields
type FullPost = PopulatedSchema<z.infer<typeof PostSchema>>;

zObjectId(options?)

Helper to create a Zod schema representing a Mongoose ObjectId.

  • options: Optional MongooseMeta for this field.

zRef(ref, schema, options?)

Helper for fields that can be either an ObjectId (unpopulated) or a populated object.

  • ref: The name of the Mongoose model being referenced.
  • schema: The Zod schema representing the populated object.
  • options: Optional MongooseMeta for this field.
const UserSchema = z.object({ name: z.string() });
const PostSchema = z.object({
  author: zRef('User', UserSchema),
});

zBuffer(options?)

Helper to create a Zod schema representing a Mongoose Buffer.

  • options: Optional MongooseMeta for this field.

When validating a Mongoose document, zBuffer() also accepts the BSON Binary value produced by document.toObject() and converts it back to a Node.js Buffer.

Isomorphic Support

@nullix/zod-mongoose is designed to be isomorphic, meaning you can use the same Zod schemas on both the frontend (browser) and backend (Node.js).

Automatic Frontend Mode

The library automatically detects the environment using package.json conditional exports. When you import from @nullix/zod-mongoose in a browser environment (detected by bundlers like Vite, Webpack, or Nuxt), it uses a specialized frontend bundle that:

  • Does not depend on mongoose.
  • Maps zObjectId() to a regex-validated string.
  • Maps zBuffer() to Uint8Array.
  • Preserves all Mongoose metadata so the same schema can be used to generate a Mongoose schema on the backend.

Legacy Manual Configuration

setFrontendMode() remains available in v3.1 but is deprecated and logs a warning when called. Conditional exports now select the correct implementation automatically.

import { setFrontendMode } from '@nullix/zod-mongoose';

// Force frontend mode (e.g. in tests or specialized environments)
setFrontendMode(true);

Migration: Remove this call. In Nuxt, continue importing shared schema helpers from @nullix/zod-mongoose; its conditional exports select the correct bundle.

genTimestampsSchema(createdAtField?, updatedAtField?)

Returns a plain object (Zod shape) with timestamp fields. This allows for easy spreading into z.object().

  • Default fields are createdAt and updatedAt.
  • Pass null to disable a specific field.
const schema = z.object({
  ...genTimestampsSchema(),
  name: z.string(),
});

Hooks

@nullix/zod-mongoose uses unjs/hookable to provide an extensible conversion process. Developers can register hooks to modify the Mongoose definition at any point of the conversion.

Registering Hooks

import { hooks } from '@nullix/zod-mongoose';

// Modify every string field to be uppercase
hooks.hook('converter:node', (context) => {
  if (context.type === 'string') {
    context.mongooseProp.uppercase = true;
  }
});

// Add a custom property after any validation mapping
hooks.hook('validation:mappers', (context) => {
  context.mongooseProp.customField = 'my-value';
});

Available Hooks

The following hook points are available:

  • converter:before: Called before starting the conversion.
  • converter:start: Called at the start of each extractMongooseDef call (recursive).
  • converter:unwrapped: Called after unwrapping a Zod schema and extracting metadata.
  • converter:node: Called for each Zod type node being processed.
  • converter:after: Called after a node's conversion is complete.
  • schema:object:before: Called before processing a z.object().
  • schema:object:field: Called for each field in a z.object().
  • schema:object:after: Called after processing a z.object().
  • schema:array:before: Called before processing a z.array(), z.set(), or z.tuple().
  • schema:array:after: Called after processing an array-like type.
  • schema:record:before: Called before processing a z.record() or z.map().
  • schema:record:after: Called after processing a record/map type.
  • schema:union:before: Called before processing a z.union() or z.discriminatedUnion(). Provides a ctx object with isSimpleUnion which can be modified to force the use of Schema.Types.Union.
  • schema:union:after: Called after processing a union.
  • registry:get:before: Called before retrieving metadata from the registry.
  • registry:get: Called when retrieving metadata from the registry.
  • registry:add: Called before adding metadata to the registry.
  • registry:added: Called after adding metadata to the registry.
  • validation:mappers: Called after mapping Zod validations to Mongoose options.
  • schema:created: Called after a mongoose.Schema instance is created (for toMongooseSchema).

Deprecated inspirations

This project was once inspired by https://github.com/Harm-Nullix/mongoose-zod.
Later on, the whole core got replaced, but if you are migrating from mongoose-zod here is what you need to know about features that are no longer supported or have changed:

  • mongooseZodCustomType(): This was previously used to directly define a Mongoose type on a Zod schema. In the new version, use withMongoose(z.any(), { type: mongoose.Schema.Types.YourType }) or specialized helpers like zObjectId() and zBuffer().
  • toZodMongooseSchema(): Previously, this returned a ZodMongoose wrapper used to generate the Mongoose schema. This function has been replaced by toMongooseSchema() which returns a full mongoose.Schema instance, or extractMongooseDef() which returns the raw Mongoose schema definition (POJO).
  • toMongooseSchema(): In the old version, this was a method on the ZodMongoose instance. Now it's a standalone function that accepts a Zod schema and returns a mongoose.Schema object.
  • ZodMongoose class: Replaced by standard Zod types with registry-stored metadata.
  • setup({ z }): No longer required. The library uses the zod/v4 registry directly and does not modify the Zod prototype.
  • .mongoose(), .mongooseTypeOptions(), .mongooseSchemaOptions(): These prototype extensions are deprecated. Use withMongoose() instead.
  • Automatic Plugin Loading: Optional peer dependencies like mongoose-lean-* are no longer automatically attached. Plugins should be applied to the Mongoose schema manually.

💖 Support the Project

If you find @nullix/zod-mongoose helpful, consider supporting its development:

  • 🩵 Sponsor on GitHub
  • ⭐ Star the repository on GitHub
  • 💬 Share it with your developer community