npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zod-to-mongo-schema

v1.1.2

Published

Convert Zod schemas to MongoDB-compatible JSON Schemas effortlessly.

Downloads

65

Readme

zod-to-mongo-schema

Convert Zod schemas to MongoDB-compatible JSON Schemas effortlessly.

license npm downloads ci code style lint tests commitlint

Overview

As your project matures, the structure of your database tends to stabilize. That's where JSON Schemas come in — they let you annotate and validate your MongoDB documents so that invalid values don't sneak in and break your app in production.

But writing JSON Schemas by hand isn't fun. As a JavaScript developer, chances are you're already using Zod to define your schemas.

Wouldn't it be great if you could just take your existing Zod schema and instantly turn it into a MongoDB-compatible JSON Schema?

That's exactly what zod-to-mongo-schema does. It takes your Zod schema and converts it into a ready-to-use JSON Schema that can be applied directly to your MongoDB collections for validation.

Installation

Note: This library expects Zod ^3.25.0 or 4.x.x as a peer dependency.

# npm
npm install zod-to-mongo-schema

# yarn
yarn add zod-to-mongo-schema

# pnpm
pnpm add zod-to-mongo-schema

Usage

A basic example

import z from "zod";
import zodToMongoSchema from "zod-to-mongo-schema";

const userSchema = z.object({
  name: z.string(),
  age: z.number().min(18),
  isAdmin: z.boolean(),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "age": {
      "type": "number",
      "minimum": 18
    },
    "isAdmin": {
      "type": "boolean"
    }
  },
  "required": ["name", "age", "isAdmin"],
  "additionalProperties": false
}

A nested Zod schema

const userSchema = z.object({
  name: z.string().meta({
    title: "User Name",
    description: "This is the name assigned to the user",
  }),
  profile: z.object({
    bio: z.string().optional(),
    followers: z.int().min(0),
  }),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "name": {
      "title": "User Name",
      "description": "This is the name assigned to the user",
      "type": "string"
    },
    "profile": {
      "type": "object",
      "properties": {
        "bio": {
          "type": "string"
        },
        "followers": {
          "minimum": 0,
          "bsonType": "long"
        }
      },
      "required": ["followers"],
      "additionalProperties": false
    }
  },
  "required": ["name", "profile"],
  "additionalProperties": false
}

Specifying BSON types with .meta()

If there's no direct Zod API for a BSON type, you can use z.unknown().meta():

const userSchema = z.object({
  _id: z.unknown().meta({ bsonType: "objectId" }),
  createdAt: z.unknown().meta({ bsonType: "date" }),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "_id": {
      "bsonType": "objectId"
    },
    "createdAt": {
      "bsonType": "date"
    }
  },
  "required": ["_id", "createdAt"],
  "additionalProperties": false
}

Only z.unknown() can be used with .meta() to specify BSON types. Using .meta() on other Zod types will throw:

const userSchema = z.object({
  _id: z.string().meta({ bsonType: "objectId" }),
});
const mongoSchema = zodToMongoSchema(userSchema);
Error: `bsonType` can only be used with `z.unknown()`.

Order of .meta() with chained methods

When chaining methods like .and(), .or(), or .nullable() on these custom fields, .meta({ bsonType }) must come first. Otherwise, the metadata will be applied to the wrapper instead of the actual field, resulting in an error or incorrect Mongo schema.

import { ObjectId } from "mongodb";

const userSchema = z.object({
  _id: z.unknown().meta({ bsonType: "objectId" }).nullable(), // correct
  // _id: z.unknown().nullable().meta({ bsonType: "objectId" }), // incorrect
});
const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "_id": {
      "anyOf": [
        {
          "bsonType": "objectId"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": ["_id"],
  "additionalProperties": false
}

Runtime validation for .meta custom fields

For runtime validation, .refine() can be applied before .meta(). This ensures the validation logic is preserved while still including the metadata:

import { ObjectId } from "mongodb";

const userSchema = z.object({
  _id: z
    .unknown()
    .refine((value) => ObjectId.isValid(value as any))
    .meta({ bsonType: "objectId" }),
  createdAt: z
    .unknown()
    .refine((value) => !Number.isNaN(new Date(value as any).getTime()))
    .meta({ bsonType: "date" }),
});

Number types

For numbers, z.number() is sufficient. It produces type: "number", which can represent integer, decimal, double, or long BSON types.

However, if you want to be specific, use:

  • z.int32() for BSON int
  • z.int() and z.uint32() for BSON long
  • z.float32() and z.float64() for BSON double
  • .meta to specify custom BSON numeric types like decimal
const userSchema = z.object({
  height: z.number(),
  age: z.int32(),
  totalPoints: z.int(),
  precision32: z.float32(),
  precision64: z.float64(),
  balance: z.unknown().meta({ bsonType: "decimal" }),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "height": {
      "type": "number"
    },
    "age": {
      "bsonType": "int"
    },
    "totalPoints": {
      "bsonType": "long"
    },
    "precision32": {
      "minimum": -3.4028234663852886e38,
      "maximum": 3.4028234663852886e38,
      "bsonType": "double"
    },
    "precision64": {
      "bsonType": "double"
    },
    "balance": {
      "bsonType": "decimal"
    }
  },
  "required": [
    "height",
    "age",
    "totalPoints",
    "precision32",
    "precision64",
    "balance"
  ],
  "additionalProperties": false
}

When .min() or .max() is used with z.int32() or z.int(), the BSON type is inferred based on range:

  • Within the 32-bit range is int
  • Above 32-bit but within 64-bit range is long
  • Beyond the 64-bit range falls back to number
const userSchema = z.object({
  smallInt: z.int().min(-100).max(100),
  mediumInt: z.int().min(-2_147_483_648).max(2_147_483_647),
  largeInt: z.int().min(-9_000_000_000_000_000).max(9_000_000_000_000_000),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "smallInt": {
      "minimum": -100,
      "maximum": 100,
      "bsonType": "int"
    },
    "mediumInt": {
      "bsonType": "int"
    },
    "largeInt": {
      "minimum": -9000000000000000,
      "maximum": 9000000000000000,
      "bsonType": "long"
    }
  },
  "required": ["smallInt", "mediumInt", "largeInt"],
  "additionalProperties": false
}

Zod's z.number(), z.float32(), and z.float64() all serialize to "type": "number" in JSON Schema. This means the original intent (float32 vs float64 vs generic number) is lost during conversion. To prevent incorrect type inference, only exact IEEE-754 float32/float64 ranges are treated as double. Any custom or partial numeric range simply falls back to "number", with its range preserved. This ensures precision is never assumed where intent is ambiguous:

const FLOAT32_MIN = -3.402_823_466_385_288_6e38;
const FLOAT32_MAX = 3.402_823_466_385_288_6e38;
const FLOAT64_MIN = -1.797_693_134_862_315_7e308;
const FLOAT64_MAX = 1.797_693_134_862_315_7e308;

const schema = z.object({
  float32: z.float32(),
  float32DefaultRange: z.number().min(FLOAT32_MIN).max(FLOAT32_MAX),
  float64: z.float64(),
  float64DefaultRange: z.number().min(FLOAT64_MIN).max(FLOAT64_MAX),
  customRange1: z.float32().min(0.1).max(99.9), // Falls back to "number"
  customRange2: z.float64().min(0.5), // Falls back to "number"
});

const mongoSchema = zodToMongoSchema(schema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "float32": {
      "minimum": -3.4028234663852886e38,
      "maximum": 3.4028234663852886e38,
      "bsonType": "double"
    },
    "float32DefaultRange": {
      "minimum": -3.4028234663852886e38,
      "maximum": 3.4028234663852886e38,
      "bsonType": "double"
    },
    "float64": {
      "bsonType": "double"
    },
    "float64DefaultRange": {
      "bsonType": "double"
    },
    "customRange1": {
      "minimum": 0.1,
      "maximum": 99.9,
      "type": "number"
    },
    "customRange2": {
      "minimum": 0.5,
      "maximum": 1.7976931348623157e308,
      "type": "number"
    }
  },
  "required": [
    "float32",
    "float32DefaultRange",
    "float64",
    "float64DefaultRange",
    "customRange1",
    "customRange2"
  ],
  "additionalProperties": false
}

Unsupported JSON Schema keywords

MongoDB's $jsonSchema operator does not support the following JSON Schema keywords:

  • $ref
  • $schema
  • default
  • definitions
  • format
  • id

These keywords, along with unknown ones, are automatically removed during conversion unless they appear as property names:

const userSchema = z.object({
  id: z.uuid(),
  name: z.string().default("Anonymous"),
  age: z.string().meta({ whatever: "trash" }),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "name": {
      "type": "string"
    },
    "age": {
      "type": "number"
    }
  },
  "required": ["id", "name", "age"],
  "additionalProperties": false
}

Unsupported Zod APIs

The following Zod APIs are not representable in JSON Schema and will throw an error if encountered:

  • z.bigint()
  • z.uint64()
  • z.int64()
  • z.symbol()
  • z.void()
  • z.date()
  • z.map()
  • z.set()
  • z.transform()
  • z.nan()
  • z.custom()

Use .meta() judiciously

Note that any number of items can be added to the object passed to .meta(), and any fields added in .meta() will override those defined in the schema:

const userSchema = z
  .object({
    name: z.string().meta({
      title: "Username",
      description: "A unique username",
      example: "johndoe",
      whatever: "trash",
    }),
  })
  .meta({ additionalProperties: true });

const jsonSchema = z.toJSONSchema(userSchema);
console.log(JSON.stringify(jsonSchema, null, 2));
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "additionalProperties": true,
  "type": "object",
  "properties": {
    "name": {
      "title": "Username",
      "description": "A unique username",
      "example": "johndoe",
      "whatever": "trash",
      "type": "string"
    }
  },
  "required": ["name"]
}

This is the intended design of the .meta() API — Zod allows arbitrary metadata.

However, zod-to-mongo-schema expects you to use it only for two purposes:

  1. To specify title and description fields:

    const userSchema = z.object({
      email: z.email().meta({
        title: "User Email",
        description: "The user's registered email address",
      }),
    });
    
    const mongoSchema = zodToMongoSchema(userSchema);
    console.log(JSON.stringify(mongoSchema, null, 2));
    {
      "type": "object",
      "properties": {
        "email": {
          "title": "User Email",
          "description": "The user's registered email address",
          "type": "string",
          "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
        }
      },
      "required": ["email"],
      "additionalProperties": false
    }
  2. To specify a custom BSON type with z.unknown if the Zod API doesn't have it:

    const userSchema = z.object({
      _id: z.unknown().meta({ bsonType: "objectId" }),
      createdAt: z.unknown().meta({ bsonType: "date" }),
    });
    
    const mongoSchema = zodToMongoSchema(userSchema);
    console.log(JSON.stringify(mongoSchema, null, 2));
    {
      "type": "object",
      "properties": {
        "_id": {
          "bsonType": "objectId"
        },
        "createdAt": {
          "bsonType": "date"
        }
      },
      "required": ["_id", "createdAt"],
      "additionalProperties": false
    }

Of course, you can choose to break the rule and still extend it beyond those two cases:

const userSchema = z.object({
  name: z.string().meta({ maxLength: 50, default: "Anonymous" }),
});

const mongoSchema = zodToMongoSchema(userSchema);
console.log(JSON.stringify(mongoSchema, null, 2));
{
  "type": "object",
  "properties": {
    "name": {
      "maxLength": 50,
      "type": "string"
    }
  },
  "required": ["name"],
  "additionalProperties": false
}

However, you cannot specify type and bsonType simultaneously for a schema, since that would be invalid for MongoDB. If you do, an error will be thrown:

const userSchema = z.object({
  _id: z.unknown().meta({ type: "boolean", bsonType: "objectId" }),
});
const mongoSchema = zodToMongoSchema(userSchema);
Error: Cannot specify both `type` and `bsonType` simultaneously.

Outside those two cases, the library assumes you know better than it — so you're fully responsible for ensuring the produced JSON Schema is valid for MongoDB.

zod-to-mongo-schema encourages you to rely on your Zod schemas as much as possible, and only step outside them for the two supported .meta() uses listed above.

The tables below show how to express common MongoDB JSON Schema patterns using standard Zod APIs.

Type mapping: MongoDB → Zod

| MongoDB | Zod | | :----------- | :---------------------------------- | | double | z.float32(), z.float64() | | string | z.string() | | object | z.object() | | array | z.array(), z.tuple() | | binData | .meta({ bsonType: "binData" }) | | objectId | .meta({ bsonType: "objectId" }) | | bool | z.boolean(), z.stringbool() | | date | .meta({ bsonType: "date" }) | | null | z.null() | | regex | .meta({ bsonType: "regex" }) | | javascript | .meta({ bsonType: "javascript" }) | | int | z.int32() | | long | z.int(), z.uint32() | | decimal | .meta({ bsonType: "decimal" }) | | number | z.number() |

Note: timestamp, minKey and maxKey are BSON types not included in the list above. They were not added as they're MongoDB internal types not intended for outside usage.

To learn more about MongoDB BSON types, check out the MongoDB docs.

Keyword mapping: MongoDB → Zod

This table is a work in progress. If you know of a Zod API that maps to a MongoDB JSON Schema keyword but it isn't here, please open a PR for it.

| MongoDB | Zod | | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | additionalItems | .rest() | | additionalProperties | .catchall(), .looseObject(), .object(), .record(), .strictObject() | | allOf | .and(), .intersection() | | anyOf | .discriminatedUnion(), .nullable(), .nullish(), .or(), .union() | | bsonType | .meta({ bsonType: "objectId" }) | | dependencies | | | description | .meta({ description: "..." }) | | enum | .enum(), .keyOf(), .literal() | | exclusiveMaximum | .lt(), .negative() | | exclusiveMinimum | .gt(), .positive() | | items | .array() | | maximum | .lte(), .max(), .nonpositive() | | maxItems | .length(), .max() | | maxLength | .length(), .max() | | maxProperties | | | minimum | .gte(), .min(), .nonnegative() | | minItems | .length(), .min(), nonEmpty() | | minLength | .length(), .min(), nonEmpty() | | minProperties | | | multipleOf | .multipleOf() | | not | .never() | | oneOf | | | pattern | .base64(), .base64url(), .cidrv4(), .cidrv6(), .cuid(), .cuid2(), .email(), .emoji(), .endsWith(), .hash(), .hex(), .hostname(), .includes(), .ipv4(), .ipv6(), .iso.duration(), .iso.date(), .iso.datetime(), .iso.time(), .lowercase(), .nanoid(), .regex(), .startsWith(), .templateLiteral(), .ulid(), .uppercase(), .uuid() | | patternProperties | | | properties | Implicitly created whenever you define a schema that has other schemas nested in it | | required | .optional(), .partial(), .required() | | title | .meta({ title: "..." }) | | type | Implicitly created whenever you define a schema | | uniqueItems | |

To learn more about MongoDB JSON Schema keywords, check out the MongoDB docs.

Further reading

I wrote a detailed post about why I built this library, the challenges I faced, and the design decisions that shaped it. Read the full article.