Schema Types and Options — A Deep Dive

A Mongoose schema is not just a list of field names — each field has a SchemaType that controls how values are stored, coerced, and validated. Understanding every SchemaType and its options in depth is what separates a schema that silently accepts bad data from one that enforces the exact shape your application expects. In this lesson you will go beyond the basics and master the full SchemaType system — including the less-common types like Map, Mixed, and nested schemas — with examples grounded in the MERN Blog data model.

All Mongoose SchemaTypes

SchemaType BSON Type Declaration Common Use
String String String or { type: String } Text fields — title, slug, email, body
Number Double / Int32 Number Counts, prices, ratings, page numbers
Boolean Boolean Boolean Flags — published, active, verified
Date Date Date Timestamps, scheduled dates, expiry times
ObjectId ObjectId mongoose.Schema.Types.ObjectId Foreign key references — author, post
Array Array [String] or [{ type: ObjectId }] Tags, roles, embedded sub-documents
Map Object { type: Map, of: String } Key-value pairs with dynamic keys — metadata, translations
Mixed Any mongoose.Schema.Types.Mixed Truly unstructured data — use sparingly
Buffer BinData Buffer Binary data — file contents, hashed tokens
Decimal128 Decimal128 mongoose.Schema.Types.Decimal128 High-precision financial amounts
UUID BinData subtype 4 mongoose.Schema.Types.UUID UUIDs as an alternative to ObjectId
Note: Mongoose performs type coercion automatically when you set a field value. If your schema says viewCount: Number and you set post.viewCount = '42', Mongoose converts the string '42' to the number 42 before saving. If coercion fails — for example setting a Number field to 'hello' — Mongoose stores NaN and a ValidationError is thrown when the document is validated. Be aware of coercion when building forms in React that send all values as strings.
Tip: Use the Map type for dynamic key-value data where you do not know the keys in advance — for example, a localizedTitle field that stores the post title in multiple languages: { en: 'Hello', fr: 'Bonjour', de: 'Hallo' }. Map types are stored as regular BSON objects but Mongoose gives you a JavaScript Map interface with .get(), .set(), and .has() methods.
Warning: Avoid the Mixed type unless you genuinely cannot model the data structure in advance. Mongoose does not validate Mixed fields at all — any value is accepted. More importantly, Mongoose does not track changes to Mixed fields automatically, so calling doc.save() after modifying a Mixed field requires explicitly calling doc.markModified('fieldName') or the change will be silently ignored.

String SchemaType — All Options

const postSchema = new mongoose.Schema({
  title: {
    type:      String,
    required:  [true, 'Title is required'],
    trim:      true,           // strip leading/trailing whitespace before saving
    lowercase: false,          // auto-lowercase (useful for email, slug)
    uppercase: false,          // auto-uppercase (useful for country codes)
    minlength: [3,   'Title must be at least 3 characters'],
    maxlength: [200, 'Title cannot exceed 200 characters'],
    match:     [/^[a-zA-Z0-9\s\-_'".,!?]+$/, 'Title contains invalid characters'],
    enum:      undefined,      // (used for fields with a fixed value set)
    default:   undefined,
  },
  status: {
    type:    String,
    enum:    {
      values:  ['draft', 'published', 'archived'],
      message: '{VALUE} is not a valid status', // {VALUE} is replaced with the actual value
    },
    default: 'draft',
  },
  slug: {
    type:      String,
    lowercase: true,     // always stored as lowercase
    trim:      true,
    unique:    true,     // creates a unique MongoDB index
    sparse:    true,     // unique index ignores null values (useful for optional unique fields)
  },
});

Number SchemaType — All Options

const postSchema = new mongoose.Schema({
  viewCount: {
    type:    Number,
    default: 0,
    min:     [0,    'View count cannot be negative'],
    max:     [1e9,  'View count seems unrealistically high'],
    validate: {
      validator: Number.isInteger,
      message:   'View count must be an integer',
    },
  },
  rating: {
    type: Number,
    min:  0,
    max:  5,
    // No default — null means "not rated yet"
  },
  price: {
    type: mongoose.Schema.Types.Decimal128, // high-precision for financial values
    default: null,
  },
});

Date SchemaType

const postSchema = new mongoose.Schema({
  publishedAt: {
    type:    Date,
    default: null,           // null means not yet published
  },
  scheduledFor: {
    type:    Date,
    default: null,
    validate: {
      validator: function (v) {
        return !v || v > new Date(); // must be in the future if set
      },
      message: 'Scheduled date must be in the future',
    },
  },
}, { timestamps: true }); // auto adds createdAt and updatedAt as Date fields

Nested Schemas — Sub-documents

// Define a reusable sub-document schema
const addressSchema = new mongoose.Schema({
  street:  { type: String, required: true },
  city:    { type: String, required: true },
  country: { type: String, required: true, default: 'US' },
  zip:     { type: String, match: /^\d{5}(-\d{4})?$/ },
}, { _id: false }); // _id: false — sub-documents don't need their own _id

// Use it as a nested field
const userSchema = new mongoose.Schema({
  name:    { type: String, required: true },
  address: addressSchema,      // single embedded sub-document
  previousAddresses: [addressSchema], // array of sub-documents
});

// The embedded address is validated as part of the parent document save
const user = await User.create({
  name: 'Jane',
  address: { street: '123 Main St', city: 'Austin', country: 'US' },
});

Map SchemaType

// Map type — dynamic keys, typed values
const postSchema = new mongoose.Schema({
  localizedTitle: {
    type: Map,
    of:   String,   // all values must be strings
    default: new Map(),
  },
  metadata: {
    type: Map,
    of:   mongoose.Schema.Types.Mixed, // values can be anything
  },
});

// Usage
const post = new Post({
  localizedTitle: new Map([
    ['en', 'Getting Started with MERN'],
    ['fr', 'Démarrer avec MERN'],
    ['de', 'Einstieg in MERN'],
  ]),
});

// Access
post.localizedTitle.get('en');      // 'Getting Started with MERN'
post.localizedTitle.set('es', '...'); // add a new key
post.localizedTitle.has('jp');      // false

// In JSON output
// "localizedTitle": { "en": "Getting Started...", "fr": "Démarrer..." }

Common Mistakes

Mistake 1 — Using Number for currency/financial fields

❌ Wrong — floating point precision issues with regular Number:

price: { type: Number }
// 0.1 + 0.2 = 0.30000000000000004 — floating point error
// Critical for prices, balances, financial calculations

✅ Correct — use Decimal128 for financial precision, or store prices as integers (cents):

priceInCents: { type: Number }          // store 1999 for $19.99 ✓
// or
price: { type: mongoose.Schema.Types.Decimal128 } // high-precision decimal ✓

Mistake 2 — Mutating a Mixed field without markModified

❌ Wrong — changes to Mixed fields are silently ignored without markModified:

post.metadata.newKey = 'value'; // modifying Mixed field
await post.save();              // Mongoose does not detect this change — not saved!

✅ Correct — call markModified after mutating Mixed or nested plain object fields:

post.metadata.newKey = 'value';
post.markModified('metadata'); // tell Mongoose the field changed ✓
await post.save();

Mistake 3 — Forgetting { _id: false } on sub-document schemas

❌ Wrong — every embedded address gets its own _id, bloating the document:

const addressSchema = new mongoose.Schema({ street: String, city: String });
// Each embedded address: { _id: ObjectId, street: '...', city: '...' }
// Usually unnecessary overhead for embedded value objects

✅ Correct — use _id: false for embedded value objects that do not need independent identification:

const addressSchema = new mongoose.Schema({ street: String, city: String }, { _id: false });
// { street: '...', city: '...' } — clean, no extra _id ✓

Quick Reference

Type Declaration Key Options
String String trim, lowercase, uppercase, minlength, maxlength, enum, match
Number Number min, max, default
Boolean Boolean default
Date Date default, min, max
ObjectId ref { type: ObjectId, ref: 'Model' } ref, required
String array [String] default: []
Sub-document nested schema or object _id: false for value objects
Map { type: Map, of: String } of (value type), default

🧠 Test Yourself

Your Post schema has viewCount: { type: Number, default: 0 }. A React form submits { viewCount: '5' } (a string). What does Mongoose store in MongoDB?