Your Zod Schema's .optional() Might Be Silently Rejecting Valid Data From MongoDB

●1 ●4 ●55
calendar_today ago • schedule3 min read

This one trips people up specifically because the two words feel interchangeable in everyday English, "optional" and "nullable" both sound like "this might not be there," and Zod treats them as two genuinely distinct, non-overlapping conditions.

What Each One Actually Validates

import { z } from 'zod';

const Schema = z.object({
  bio: z.string().optional(), // allows the key to be MISSING, or a string. Does NOT allow null.
  avatar: z.string().nullable(), // requires the key to be PRESENT, as a string OR null. Rejects missing.
});

.optional() accepts undefined, meaning the field can be genuinely absent from the object entirely, but if it's present, it must match the base type, never null. .nullable() accepts null specifically, but the key itself still needs to actually be present in the object, just potentially holding the value null rather than a real value. These aren't two flavors of the same leniency, they're validating two different, non-overlapping conditions, and passing the wrong kind of absence to either one fails validation.

Where This Actually Breaks With MongoDB Specifically

MongoDB documents very commonly store an explicit null for a field that has no value, rather than omitting the key entirely, especially for fields that were part of the original schema design but just haven't been set for a given document yet.

// A real document from MongoDB
{
  _id: '...',
  name: 'Alex',
  bio: null, // explicitly null, not missing
}
// ❌ This schema expects the key to be absent, not present-and-null
const UserSchema = z.object({
  name: z.string(),
  bio: z.string().optional(),
});

UserSchema.safeParse(userFromDb); // fails: bio received null, optional() doesn't accept null

bio: null is a completely valid, common shape coming straight out of MongoDB, and .optional() rejects it anyway, since .optional() only ever expected undefined, never null. The validation failure here isn't catching a real data problem, it's a mismatch between what Zod's .optional() actually means and what the developer assumed it meant when writing the schema against real database output.

The Fix: Match the Schema to What the Data Actually Does

const UserSchema = z.object({
  name: z.string(),
  bio: z.string().nullable(), // correctly matches MongoDB's explicit null
});

For a field that MongoDB stores as explicit null when empty, .nullable() is the correct validator, not .optional(). If a field could genuinely be either missing entirely or explicitly null, both conditions need to be allowed together:

bio: z.string().nullable().optional(), // accepts a string, null, OR a missing key

Chaining both is completely valid and often the most accurate reflection of real-world data that's passed through several code paths, some of which omit the key, others of which explicitly set it to null.

Why This Doesn't Show Up Until Real Data Hits It

A schema tested only against hand-written test objects during development, where the developer naturally omits fields they don't care about rather than explicitly setting them to null, never actually exercises this gap. The validation failure only shows up once real documents, containing MongoDB's genuine, common pattern of explicit null values, flow through the same schema, often well after the schema was written and seemingly working correctly.

A Quick Way to Check Which One You Actually Need

Query a real, representative document from your actual collection and look at the field in question directly. If it shows up as null in the raw document, you need .nullable(). If the key is genuinely absent from documents where it has no value, you need .optional(). Guessing based on the field's name or what "feels right" is exactly how this mismatch gets introduced in the first place, checking the real data removes the ambiguity entirely.

const doc = await User.findOne({}).lean();
console.log(Object.keys(doc).includes('bio'), doc.bio);
// If bio is a key with value null → nullable()
// If bio is simply absent as a key → optional()

The Actual Rule

.optional() and .nullable() are not interchangeable leniency settings, they validate two distinct, specific shapes of absence, and MongoDB's own default behavior of storing explicit null means .nullable() is very often the one you actually need for fields coming straight out of a database, not .optional(), even though "optional" is the word that intuitively comes to mind first. When a schema seems to be rejecting data that looks completely fine in your database, this mismatch is one of the first things worth checking.

I ran into exactly this building the MongoDB-backed dashboards covered in the MongoDB patterns I use in every Next.js project, and it's one of those small schema details worth getting right once rather than re-discovering on every new project.


If a Zod schema is rejecting data that looks completely valid sitting in your actual database, check whether it's this exact .optional() versus .nullable() mismatch before assuming the data itself is somehow wrong. Drop what you find in the comments.

Get the templates: https://pixelanas.gumroad.com


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

Merancang Backend Bisnis ISP: API Pelanggan, Paket Internet, Invoice, dan Tiket Support

Masbadar - Mar 13

That .lean() Query Optimization Might Be Leaking Password Hashes in Your API Response

Anas Sheikh - Sep 11

Next.js Automatically Dedupes fetch() Calls. Your MongoDB Queries Don't Get That for Free.

Anas Sheikh - Sep 4

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3
chevron_left
1.1k Points • 60 Badges
Pakistan • pixelanas.com
39Posts
9Comments
6Connections
Full-stack developer building premium Next.js templates & web apps. Specializing in GSAP animations,... Show more

Related Jobs

View all jobs →

Commenters (This Week)

7 comments
3 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!