Playground
Api

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.

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

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

const ActivityZodSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('login'),
    timestamp: z.date(),
    ip: z.string()
  }),
  z.object({
    type: z.literal('post_create'),
    timestamp: z.date(),
    postId: zObjectId()
  }),
]);

const ActivitySchema = toMongooseSchema(ActivityZodSchema);
// Mongoose will create a base schema with 'timestamp' field
// and two discriminators ('login', 'post_create') for the other fields.

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 }