v3.3.1
Playground
API Reference

Advanced Types

Complex data modeling with Discriminators, Unions, BigInt and Composite IDs.

Advanced Types

@nullix/zod-mongoose provides robust support for Mongoose's most powerful features, including discriminators, complex unions, and native BigInt.

Discriminators

Mongoose discriminators are a schema inheritance mechanism that allows you to have multiple models with overlapping schemas on top of the same underlying MongoDB collection.

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 must provide modelName or collection. Mongoose registers discriminator model names globally, so this prevents a discriminator such as clicked from colliding with a standalone model. The model name affects only the Mongoose model name; the Zod discriminator value stored in MongoDB is unchanged.

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, withMongoose, 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.

There are four relevant values:

  • Discriminator key: type, the property used to select a branch.
  • Discriminator value: clicked or viewed, the value stored in MongoDB.
  • Base model name: Activity, provided through modelName.
  • Discriminator model name: Activity_clicked or Activity_viewed, generated by the library.

This follows Mongoose's discriminator API: the model name and stored discriminator value are separate concepts. zod-mongoose passes the generated model name as the first argument and the original Zod value through Mongoose's { value: ... } option. Omitting both modelName and collection for a top-level discriminated union throws a helpful error instead of risking a model-name collision.

Accessing discriminator models

toMongooseSchema() returns the base mongoose.Schema, not a model. Compile it into the base model first; Mongoose then 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>>;

// After connecting to MongoDB:
const event = await ClickedEventModel.findOne({target: 'button'});

The discriminator model uses the same activities collection and automatically adds the discriminator filter (type: 'clicked'). Its registered Mongoose model name is Activity_clicked; the value stored in MongoDB remains clicked.

Manual Discriminators

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

const baseSchema = withMongoose(
  z.object({
    name: z.string(),
    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 }));

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' }).

const schema = z.object({
  largeNumber: z.bigint(),
  longInt: withMongoose(z.bigint(), { type: 'Long' }),
});

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 }