# Why MongoZod

> MongoZod as a Mongoose alternative for TypeScript, compared with Mongoose, Typegoose, the driver and Zod by hand and other libraries, the Mongoose issues it solves and when not to use it.

MongoZod is a Mongoose alternative for TypeScript: one Zod schema validates every write, types
every query and defines the inputs of each endpoint, on the official MongoDB driver.

A project on MongoDB and TypeScript usually describes each document three times: in a Mongoose
schema, in a TypeScript type, and in the Zod schemas that validate what its endpoints receive. They
drift apart, and each one checks something different:

```ts nocheck
import mongoose from 'mongoose'
import * as z from 'zod'

// For TypeScript
interface Post {
  title: string
  status: 'draft' | 'published'
  tags: string[]
  createdAt: Date
  updatedAt: Date
}

// For MongoDB: the same fields again
const postSchema = new mongoose.Schema<Post>(
  {
    title: { type: String, required: true, minlength: 1 },
    status: { type: String, enum: ['draft', 'published'], default: 'draft' },
    tags: { type: [String], default: [] }
  },
  { timestamps: true }
)

const PostModel = mongoose.model<Post>('Post', postSchema)

// For the endpoint: the same fields a third time
const createPostInput = z.object({

  title: z.string()
    .min(1),

  status: z.enum(['draft', 'published'])
    .optional(),

  tags: z.string()
    .array()
    .optional()
})
```

MongoZod keeps one description, the Zod schema, and builds the rest from it: the model validates
every write with it, the types of every query and result come from it, and the inputs of the
endpoints are derived from it:

```ts separate
import { MongoClient, ObjectId } from 'mongodb'
import { mongozod } from 'mongozod'
import * as mz from 'mongozod/mzod'
import * as z from 'zod'

const client = new MongoClient('mongodb://localhost:27017')

const postSchema = z.object({

  _id: mz.objectId()
    .default(() => new ObjectId()),

  title: z.string()
    .min(1),

  status: z.enum(['draft', 'published'])
    .default('draft'),

  tags: z.string()
    .array()
    .default([]),

  createdAt: mz.date(),

  updatedAt: mz.date()
})

const PostsModel = mongozod({

  collection: 'posts',

  db: client.db(),

  schema: postSchema,

  timestamps: true
})

const createPostInput = postSchema.pick({
  title: true,
  status: true,
  tags: true
})

type Post = z.output<typeof postSchema>
```

## Compared with Mongoose

|                   | Mongoose 9                                                                                                                                                                                                                           | MongoZod                                                                                                                                            |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Schema            | A Mongoose schema, apart from what validates your endpoints                                                                                                                                                                          | One Zod schema for the document, its types and the inputs of your endpoints                                                                         |
| Document type     | [Inferred](https://mongoosejs.com/docs/typescript/schemas.html) only from a schema written inline; otherwise an interface kept in sync by hand, or the classes and decorators of [Typegoose](https://typegoose.github.io/typegoose/) | The type of the Zod schema, wherever you declare it, with no interface and no decorators                                                            |
| Update validation | [Off by default](https://mongoosejs.com/docs/validation.html#update-validators), and only on `$set`, `$unset`, `$push`, `$addToSet`, `$pull` and `$pullAll`                                                                          | Always, on every operator, with refinements, and on the document an upsert would insert                                                             |
| Required fields   | [Not checked by an update](https://mongoosejs.com/docs/validation.html#update-validators-only-run-on-updated-paths) unless it `$unset`s them                                                                                         | An upsert fails when the document it would insert misses one                                                                                        |
| Filters           | [Cast at runtime](https://mongoosejs.com/docs/tutorials/query_casting.html): a value that cannot be cast throws a `CastError`                                                                                                        | Typed: a value of the wrong type does not compile, and what clients send is parsed by `mz`                                                          |
| Results           | Documents that track changes, or plain objects with `lean()`                                                                                                                                                                         | The driver's plain objects, typed by the projection and `populate`                                                                                  |
| Endpoint inputs   | Not covered                                                                                                                                                                                                                          | [`mongozod/mzod`](https://mongozod.dev/docs/guides/endpoint-inputs.md): MongoDB syntax from clients, limited to what each endpoint declares                                |
| Indexes           | [Created when the app starts](https://mongoosejs.com/docs/guide.html#indexes), unless you turn `autoIndex` off                                                                                                                       | Created by [`sync()`](https://mongozod.dev/docs/guides/sync.md) when you call it, or on the first operation with the opt-in [`autoIndex`](https://mongozod.dev/docs/plugins/auto-index.md) plugin |
| Driver            | Mongoose's own API on top of the driver                                                                                                                                                                                              | The driver's names, options and results, and the driver itself for everything else                                                                  |

Both libraries can share one connection, so a project can
[move one model at a time](https://mongozod.dev/docs/mongoose/index.md). The [cheatsheet](https://mongozod.dev/docs/mongoose/cheatsheet.md) maps
each Mongoose feature to its equivalent, and [Differences](https://mongozod.dev/docs/mongoose/differences.md) lists what
works another way.

## Compared with the driver and Zod by hand

The driver with a Zod schema already validates an insert: parse the document, then insert the
result. The rest is what a team ends up writing again in each project:

- **Updates**: an update is a set of operators on paths, not a document. MongoZod validates the
  value of each path with the schema of that field, applies the rules of each operator (`$inc` on
  numbers, `$unset` on optional fields, array operators on arrays) and, for an upsert, builds and
  validates the document MongoDB would insert.
- **Types**: the driver types a filter loosely. MongoZod checks every filter, update, projection and
  sort against the schema, dotted paths included, and narrows the result of a projection.
- **Relations and computed fields**: [`populate`](https://mongozod.dev/docs/guides/populate.md),
  [virtuals](https://mongozod.dev/docs/guides/virtuals.md) and [timestamps](https://mongozod.dev/docs/guides/timestamps.md), each typed in the
  reads that use it.
- **Inputs**: a client that sends a filter, a sort or a page needs an allow list per field, a
  parser for each value and a type that fits the driver. [`mz`](https://mongozod.dev/docs/guides/endpoint-inputs.md)
  builds them from the schema.
- **Cross-cutting rules**: [plugins](https://mongozod.dev/docs/guides/plugins.md) wrap every operation, like the official
  [soft delete](https://mongozod.dev/docs/plugins/soft-delete.md).

## Compared with other libraries

Other libraries that work with MongoDB and TypeScript, and what MongoZod chooses instead:

- **[Papr](https://github.com/plexinc/papr)** is also a thin layer over the driver. It defines
  schemas with its own helpers and validates writes with a `$jsonSchema` on the server. MongoZod
  validates each write in the app with Zod, refinements and transforms included, and can also apply
  a `$jsonSchema` generated from the schema with [`sync()`](https://mongozod.dev/docs/guides/sync.md).
- **[zod-mongoose](https://github.com/Harm-Nullix/zod-mongoose)** writes Mongoose schemas with Zod,
  and Mongoose runs underneath. MongoZod works on the driver, with its names, options and results.
- **[Prisma](https://www.prisma.io/docs/orm/v7/core-concepts/supported-databases/mongodb)**
  describes the data in its own schema language and generates a client. Prisma ORM 7 does not
  support MongoDB yet, and its docs point to 6.19 for it. MongoZod keeps the description in
  TypeScript, as a Zod schema, with nothing to generate.
- **[TypeORM](https://typeorm.io/docs/drivers/mongodb)** and
  **[MikroORM](https://mikro-orm.io/docs/usage-with-mongo)** model entities as classes with
  decorators, and TypeORM calls its MongoDB support basic. MongoZod reads and writes plain objects,
  with no classes and no decorators.

## Mongoose issues it solves

Open issues of Mongoose that MongoZod handles by design:

- **Validating updates**: the required fields of an upsert
  ([#7037](https://github.com/Automattic/mongoose/issues/7037)), the subdocuments of a
  `findOneAndUpdate` ([#5234](https://github.com/Automattic/mongoose/issues/5234)) and every error
  of an update at once, not only the first
  ([#3037](https://github.com/Automattic/mongoose/issues/3037)).
- **Types**: projections with dotted paths
  ([#16575](https://github.com/Automattic/mongoose/issues/16575)) and a typed `populate`
  ([#16010](https://github.com/Automattic/mongoose/issues/16010)).
- **Queries**: a filter value outside an enum does not compile
  ([#15581](https://github.com/Automattic/mongoose/issues/15581)).
- **Discriminators**: several levels, the most upvoted open issue
  ([#2851](https://github.com/Automattic/mongoose/issues/2851), also
  [#8361](https://github.com/Automattic/mongoose/issues/8361) and
  [#9756](https://github.com/Automattic/mongoose/issues/9756)), with a `z.discriminatedUnion`
  inside another. See [Document types](https://mongozod.dev/docs/guides/document-types.md).
- **Populate**: a `limit` per document
  ([#15863](https://github.com/Automattic/mongoose/issues/15863)) and a reference stored in another
  key ([#9034](https://github.com/Automattic/mongoose/issues/9034)).
- **Field types**: GeoJSON ([#6606](https://github.com/Automattic/mongoose/issues/6606)) and arrays
  without repeated values ([#8082](https://github.com/Automattic/mongoose/issues/8082),
  [#12916](https://github.com/Automattic/mongoose/issues/12916)), with
  [`mz.set()`](https://mongozod.dev/docs/guides/bson-types.md).
- **Soft delete**, as an official plugin
  ([#7304](https://github.com/Automattic/mongoose/issues/7304)).
- **Explicit `sync()`**: indexes and collections are not created on their own
  ([#15803](https://github.com/Automattic/mongoose/issues/15803),
  [#13546](https://github.com/Automattic/mongoose/issues/13546)), unless you opt in to
  [`autoIndex`](https://mongozod.dev/docs/plugins/auto-index.md) for the indexes.

Mongoose's types have also slowed editors down
([#10349](https://github.com/Automattic/mongoose/issues/10349)). MongoZod's CI fails when the type
instantiations of a typical project go over a budget, so a release cannot make your editor slower
without anyone noticing.

## When not to use it

MongoZod is not the right fit, or not yet, when a project needs:

- **Documents with methods and change tracking**, like Mongoose's `doc.save()`. MongoZod writes with
  explicit operations, as the driver does.
- **Filters cast at runtime** from untyped values. MongoZod types them instead, and parses what
  clients send with `mz`.
- **Zod 3**, **Node.js before 22.15** or **TypeScript before 5.5**.
- Some Mongoose features that are planned but not there yet: `select: false`, `immutable` and
  `alias` on fields, optimistic concurrency with a version field, field-level encryption, and a
  plugin that keeps references consistent on delete.