Reusing Object Schemas
Reusing Object Schemas
A shared z.object() can be used as a root schema, a nested field, or an array element. Each time it is nested without options, Mongoose creates a subschema with its own _id.
If several fields should omit that ID, define a named variant once and import it wherever needed. Keep the original schema for fields that should retain the default. withMongoose attaches metadata to the schema instance, so calling it directly on an imported shared object changes all uses of that instance. safeExtend({}) creates a separate Zod object with the same fields for the variant.
For application code, the key line is withMongoose(AddressSchema.safeExtend({}), { schema: { _id: false } }). The complete repository script below shows how the configured variant behaves in a field and an array.
Define the shared schema and its no-ID variant
import {z} from 'zod/v4';
import {withMongoose} from '../../src/index.js';
export const AddressSchema = z.object({
street: z.string(),
city: z.string(),
});
// safeExtend makes a separate Zod instance. Metadata stays on this variant.
export const AddressWithoutIdSchema = withMongoose(AddressSchema.safeExtend({}), {
schema: {_id: false, id: false},
});
In an application, import withMongoose from @nullix/zod-mongoose. The relative import above lets this repository's script run against the local source.
Use each variant where it belongs
The user schema uses the no-ID variant for one address and an array of addresses. The audit schema uses the original, so its snapshot gets the default subschema ID. You do not need to wrap the no-ID variant again at each use site.
import {z} from 'zod/v4';
import {toMongooseSchema} from '../../src/index.js';
import {AddressSchema, AddressWithoutIdSchema} from './address.js';
export const UserZodSchema = z.object({
name: z.string(),
shippingAddress: AddressWithoutIdSchema,
previousAddresses: z.array(AddressWithoutIdSchema),
});
export const AuditZodSchema = z.object({
addressSnapshot: AddressSchema,
});
export const UserMongooseSchema = toMongooseSchema(UserZodSchema);
export const AuditMongooseSchema = toMongooseSchema(AuditZodSchema);
Inspect the resulting documents
Run the script from the repository root. It builds models and documents locally and requires no database connection.
bun run packages/core/examples/reused-object-schemas/inspect.ts
/* eslint-disable no-console */
import assert from 'node:assert/strict';
import mongoose from 'mongoose';
import {UserMongooseSchema, AuditMongooseSchema} from './models.js';
const User = mongoose.model('ReusableUserExample', UserMongooseSchema);
const Audit = mongoose.model('ReusableAuditExample', AuditMongooseSchema);
const address = {street: 'Main Street', city: 'Amsterdam'};
const user = new User({
name: 'Ada',
shippingAddress: address,
previousAddresses: [address],
});
const audit = new Audit({addressSnapshot: address});
assert.ok(user._id instanceof mongoose.Types.ObjectId);
assert.equal(user.get('shippingAddress._id'), undefined);
assert.equal(user.get('previousAddresses.0._id'), undefined);
assert.ok(audit.get('addressSnapshot._id') instanceof mongoose.Types.ObjectId);
assert.equal(UserMongooseSchema.path('shippingAddress').schema.path('_id'), undefined);
assert.equal(UserMongooseSchema.path('previousAddresses').schema.path('_id'), undefined);
assert.ok(AuditMongooseSchema.path('addressSnapshot').schema.path('_id'));
console.log('User: root has _id; shippingAddress and previousAddresses do not');
console.log('Audit: addressSnapshot still has _id');
If every nested use of an imported schema should omit IDs, you can attach { schema: { _id: false, id: false } } where the schema is defined and export that configured instance directly. Create a separate variant when different uses need different options.