@palmetto/zod-mongoose-schema
Provides a builder to convert zod schemas to mongoose schemas.
Installation
First install peer dependencies:
yarn add zod
This package requires zod v4 schemas, you can either install v4 directly.
or: import { z } from "zod/v4" in version 3.25.76+ if you still have dependencies that require zod v3 schemas.
Note: zod@^4.1.5 or later is required for mongodb encryption to work correctly
yarn add @palmetto/zod-mongoose-schema
Getting started
Usage
Create a simple schema
import { z } from "zod";
import { SchemaWithId } from "@palmetto/zod-mongoose-schema";
export const UserEntitySchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.email(),
phoneNumber: z.string().optional(),
loginCount: z.number().int().default(0),
});
export type User = SchemaWithId<typeof UserEntitySchema>;
export type NewUser = Omit<User, "_id", "loginCount">;
export const UserSchema = schemaBuilder.build(UserEntitySchema);
Create re-usable schemas
import { z } from "zod";
import { SchemaWithId } from "@palmetto/zod-mongoose-schema";
const MetaEntitySchema = z
.object({
createdAt: z.date(),
createdBy: z.string(),
})
.default(() => ({
createdAt: new Date(),
createdBy: "anonymous",
}));
export const QuoteEntitySchema = z.object({
meta: MetaEntitySchema,
});
export const QuoteSchema = schemaBuilder.build(QuoteEntitySchema);
Create a discriminated Schema
SchemaBuilder supports both top-level and subdocument Schema discriminators using z.discriminatedUnion.
Some rules must be followed to allow SchemaBuilder to generate these Schemas automatically.
export enum PricingProductType {
PowerPurchase = "POWER_PURCHASE",
Loan = "LOAN",
}
export const BaseQuotePricingEntitySchema = z.object({
firstYearMonthlyPayment: z.number(),
optionId: z.string(),
productId: z.string(),
provider: z.enum(PricingProvider),
});
export const QuotePricingEntitySchema = z.discriminatedUnion("productType", [
BaseQuotePricingEntitySchema.extend({
productType: z.literal(PricingProductType.PowerPurchase),
rateEscalator: z.number(),
kwhRate: z.number(),
ppwRate: z.number(),
totalEpcPayment: z.number(),
}),
BaseQuotePricingEntitySchema.extend({
productType: z.literal(PricingProductType.Loan),
apr: z.number(),
termInMonths: z.number(),
}),
]);
export type QuotePricing = z.infer<typeof QuotePricingEntitySchema>;
export type PowerPurchaseQuotePricing = Extract<
QuotePricing,
{ productType: PricingProductType.PowerPurchase }
>;
export type LoanQuotePricing = Extract<
QuotePricing,
{ productType: PricingProductType.Loan }
>;
const QuotePricingSchema = schemaBuilder.build(QuotePricingEntitySchema);
const QuoteSchema = schemaBuilder.build(
z.object({
pricing: QuotePricingSchema.optional(),
}),
);
Please note for libraries that also provide discriminator support (eg. NestJS), those options do not need to be used and should be ignored. The Schema built by SchemaBuilder already has the discriminators pre-configured.
Registries
SchemaBuilder uses zod v4 registries to manage schema customization. The builder ships with built-in registries for common mongoose types, and also allows adding your own.
Create an ObjectId field
Use idRegistry
The idRegistry built-in registry marks a field as an ObjectId and optionally allows passing a ref.
import { idRegistry } from "@palmetto/zod-mongoose-schema";
import { Types } from "mongoose";
export const QuoteEntitySchema = z.object({
assignedUserId: z
.instanceof(Types.ObjectId)
.register(idRegistry, { ref: "User" })
.optional(),
});
Please note, the .register MUST be chained off the instanceof schema, if you chain off something else like .optional you will get a Typescript error. For more information, see: Metadata and Registries: Constraining Schema Types.
Use ObjectIdSchema
The ObjectIdSchema creates a zod field that's an ObjectId and registered to the idRegistry. Note: You lose the ability to specify the ref.
import { ObjectIdSchema } from "@palmetto/zod-mongoose-schema";
export const QuoteEntitySchema = z.object({
assignedUserId: ObjectIdSchema.optional(),
});
Create a fully custom Schema field
The schemaRegistry built-in registry allows passing in a fully customized mongoose Schema as-is to use as a subdocument.
import { schemaRegistry } from "@palmetto/zod-mongoose-schema";
import { Schema } from "mongoose";
const MetaSchema = new Schema();
type Meta = {
};
export const QuoteEntitySchema = z.object({
meta: z.custom<Meta>().register(schemaRegistry, { schema: MetaSchema }),
});
Create and customize subdocuments
The subSchemaRegistry built-in registry allows customizing schema options for subdocuments.
import { subSchemaRegistry } from "@palmetto/zod-mongoose-schema";
export const QuoteEntitySchema = z.object({
submissions: z
.object({
status: z.enum(QuoteSubmissionStatus),
})
.register(subSchemaRegistry, { _id: true })
.array()
.optional(),
});
Create a custom registry
import { RegistryEntry } from "zod-mongoose-schema";
import { z } from "zod";
export type BigIntRegistryMetadata = undefined;
export type BigIntRegistryTarget = z.$ZodBigInt;
export const bigIntRegistry = z.registry<
BigIntRegistryMetadata,
BigIntRegistryTarget
>();
schemaBuilder.addRegistry({
registry: bigIntRegistry,
filter: (type) => type._zod.def.type === "bigint",
buildSchema: (type, _meta, context) => ({
type: BigInt,
required: !(context.fieldOptions?.isOptional ?? false),
default: context.fieldOptions?.defaultValue,
validate: [
{
validator: (input: unknown) => z.safeParse(type, input).success,
},
],
}),
});
const EntitySchema = z.object({
foo: z.bigint().register(bigIntRegistry),
});
Encryption
You can enable client-side field-level encryption for many zod schema types. When using client-side encryption you must also
set the autoEncryption settings when creating the mongo connection so that encryption happens automatically within the mongodb driver.
For details, see @palmetto/mongodb-encryption
Specifying an encrypted field
const ModelSchema = z.object({
...
name: z.string(),
ssn: z
.string()
.register(encryptedRegistry, { algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Random', keyId: ssnDataEncryptionKeyId })
.optional(),
ssnLast4: z
.string()
.register(encryptedRegistry, { algorithm: 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic', keyId: ssnLast4DataEncryptionKeyId })
.optional(),
...
});
const ModelMongoSchema = schemaBuilder.build(ModelSchema, {
versionKey: false,
encryptionType: "csfle",
});
const Model = mongoose.model('collection-name', ModelMongoSchema);
const model = await Model.create({
name: 'John Doe',
ssn: '123-45-6789',
ssnLast4: '6789',
});
const found = await Model.findOne({_id: model._id});
expect(found).toBeDefined();
expect(found.ssn).toBe('123-45-6789');
expect(found.ssnLast4).toBe('6789');
const findBySsn = await Model.findOne({ssn: '123-45-6789'});
expect(findBySsn).toBeUndefined();
const findBySsnLast4 = await Model.findOne({ssnLast4:'6789'});
expect(findBySsnLast4).toBeDefined();
Supported zod types for encryption
The following zod types can be encrypted:
z.object()
z.discriminatedUnion()
z.string()
z.date()
z.boolean()
z.int()
z.int32()
z.float32()
z.float64()
z.instanceof(Buffer)
z.literal()
z.enum()
z.map()
z.array()
z.instanceof(mongoose.Types.ObjectId).register(idRegistry)
Encryption options
See EncryptedMetaData in encryption-registry.ts
algorithm: AEAD_AES_256_CBC_HMAC_SHA_512-Random | AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic (default: AEAD_AES_256_CBC_HMAC_SHA_512-Random)
- when
AEAD_AES_256_CBC_HMAC_SHA_512-Random:
- the same plain value will encrypt to different binData each time
- more secure storage since all cipher text is different
- unable to be queried against
- when
AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic:
- the same plain value will encrypt to the same cipher text
- this is less secure since it can show that two or more records have the same plain value when both cipher text are the same
- however, the property can be queried against with normal mongodb filters
keyId: UUID
- This property contains the UUID of the data encryption key created with mongodb's ClientEncryption object.
- You must know the keyId before creating the schema object, so you must also manage the keys on your own.
- Or, leave this field out and let @palmetto/mongdb-encryption manage the keys for you.
More details about the algorithm at MongoDB encryption algorithms)
For more information about encrypted schemas at Encryption Schemas
More documentation