MongoDB $project Stage

Beginner
⏱️ 12 min read
📚 Updated: Jul 2026
🎯 5 Examples
Aggregation

What You’ll Learn

The $project stage reshapes documents in a pipeline—keeping only the fields you need, dropping sensitive data, renaming columns, and computing new values. It is one of the most-used stages for API responses, reports, and pre-export cleanup.

01

Include (1)

Whitelist fields.

02

Exclude (0)

Drop fields.

03

Rename

new: "$old"

04

Expressions

Computed values.

05

After $group

Flatten _id.

06

vs $addFields

Reshape vs add.

Definition and Usage

$project is a transformation stage that outputs a new document shape for each input document. You control which fields appear, what they are named, and how their values are computed.

💡
Beginner tip

Two common modes:
Inclusion — list fields with 1: only these fields pass through (plus _id unless you set _id: 0).
Exclusion — list fields with 0: everything except these fields passes through.
Do not mix 1 and 0 on regular fields in the same $project.

Use $project before returning results to a client, before $out or $merge to strip internal fields, or after $group to rename _id into a readable column like region or category.

📝 Syntax

$project maps output field names to inclusion flags, field paths, or expressions:

mongosh
{
  $project: {
    <field1>: <1 | 0 | expression | "$path">,
    <field2>: <1 | 0 | expression | "$path">,
    ...
  }
}

Syntax Rules

  • 1 — include this field (inclusion mode). Unlisted fields are dropped.
  • 0 — exclude this field (exclusion mode). Unlisted fields are kept.
  • _id: 0 — allowed in inclusion mode to hide the default _id.
  • RenamenewName: "$oldName" copies a field under a new key.
  • Expressionstotal: { $multiply: ["$price", "$qty"] } computes a value.
  • No mix — do not combine 1 and 0 on non-_id fields in one stage.
mongosh
db.products.aggregate([
  { $match: { inStock: true } },
  {
    $project: {
      _id: 0,
      name: 1,
      price: 1,
      category: 1
    }
  }
])

⚡ Quick Reference

QuestionAnswer
What it doesReshapes documents—include, exclude, rename, compute fields
Include modeFields set to 1; others dropped (except _id by default)
Exclude modeFields set to 0; all other fields kept
Hide _id_id: 0 in inclusion projection
Rename fielddisplayName: "$name"
vs $addFields$project reshapes; $addFields adds without dropping others
Whitelist
name: 1,
price: 1,
_id: 0

Include only

Blacklist
cost: 0,
internalSku: 0

Exclude only

Rename
title: "$name"

Copy path

Compute
total: {
  $multiply: [...]
}

Expression

🧰 Common $project Patterns

Each key in the $project document becomes an output field. Values determine how that field is built:

Field inclusion 1

Pass through an existing field unchanged. In inclusion mode, only listed fields (and _id unless suppressed) appear in output.

{ name: 1, price: 1, _id: 0 }
Field exclusion 0

Remove specific fields while keeping everything else—handy when you only need to hide a few internal columns.

{ cost: 0, supplierId: 0 }
Rename / copy Path

Assign a field path string to create a new key from an existing value—common for API-friendly names.

{ productName: "$name",
  unitPrice: "$price" }
Computed field Expression

Use aggregation operators to derive values—math, strings, dates, or conditionals.

{ margin: { $subtract: ["$price", "$cost"] },
  onSale: { $lt: ["$price", 100] } }

Examples Gallery

Sample products collection—inclusion and exclusion projections, rename and computed fields, post-$group reshaping, and comparison with $addFields.

📚 Getting Started

Product catalog for projection labs.

Example 1 — Products collection

mongosh
db.products.drop()

db.products.insertMany([
  { name: "Widget Pro",  category: "Electronics", price: 299, cost: 180, inStock: true,  rating: 4.8, internalSku: "WP-001" },
  { name: "Widget Lite", category: "Electronics", price: 99,  cost: 55,  inStock: true,  rating: 4.2, internalSku: "WL-002" },
  { name: "Office Chair",category: "Furniture",   price: 450, cost: 280, inStock: false, rating: 4.5, internalSku: "OC-003" },
  { name: "Desk Lamp",   category: "Furniture",   price: 65,  cost: 30,  inStock: true,  rating: 3.9, internalSku: "DL-004" },
  { name: "USB Hub",     category: "Electronics", price: 45,  cost: 50,  inStock: true,  rating: 4.0, internalSku: "UH-005" }
])

db.products.createIndex({ category: 1, inStock: 1 })

How It Works

Five products with public fields (name, price) and internal fields (cost, internalSku)—enough to demonstrate inclusion, exclusion, and computed projections.

Example 2 — Include only public catalog fields (whitelist)

mongosh
db.products.aggregate([
  { $match: { inStock: true } },
  {
    $project: {
      _id: 0,
      name: 1,
      category: 1,
      price: 1,
      rating: 1
    }
  },
  { $sort: { price: -1 } }
])

// Inclusion mode: only listed fields appear
// cost, internalSku, inStock dropped automatically
// _id: 0 hides MongoDB ObjectId from API response
// Office Chair excluded (inStock: false) by $match

How It Works

When you set fields to 1, MongoDB treats it as inclusion mode—only those fields (plus _id unless _id: 0) survive. Ideal for slim API payloads.

Example 3 — Exclude internal fields (blacklist)

mongosh
db.products.aggregate([
  {
    $project: {
      cost: 0,
      internalSku: 0
    }
  }
])

// Exclusion mode: everything EXCEPT cost and internalSku
// name, price, category, inStock, rating, _id all kept
// Use when you only need to hide a few sensitive fields

How It Works

Exclusion mode (0) is faster to write when most fields are safe to expose. You cannot mix cost: 0 with name: 1 in the same stage—that triggers a MongoDB error.

📈 Practical Patterns

Rename, compute, and flatten grouped results.

Example 4 — Rename fields and compute margin + sale flag

mongosh
db.products.aggregate([
  { $match: { inStock: true } },
  {
    $project: {
      _id: 0,
      productName: "$name",
      category: 1,
      price: 1,
      margin: { $subtract: ["$price", "$cost"] },
      isBudget: { $lt: ["$price", 100] },
      stockStatus: {
        $cond: {
          if: "$inStock",
          then: "Available",
          else: "Out of stock"
        }
      }
    }
  },
  { $sort: { margin: -1 } }
])

// productName renames name via "$name" path
// margin = price - cost (USB Hub → negative margin)
// isBudget: true when price < 100
// $cond builds a readable stockStatus string

How It Works

In inclusion-style $project, you can mix literal includes (category: 1), renames (productName: "$name"), and expressions. Fields not listed are omitted from output.

Example 5 — Reshape after $group (flatten _id)

mongosh
db.products.aggregate([
  {
    $group: {
      _id: "$category",
      avgPrice: { $avg: "$price" },
      productCount: { $sum: 1 },
      maxRating: { $max: "$rating" }
    }
  },
  {
    $project: {
      _id: 0,
      category: "$_id",
      avgPrice: { $round: ["$avgPrice", 2] },
      productCount: 1,
      maxRating: 1
    }
  },
  { $sort: { avgPrice: -1 } }
])

// $group puts category in _id
// $project renames _id → category for clean JSON
// $round formats avgPrice to two decimals
// Classic pattern before $out or API response

How It Works

After $group, the grouping key lives in _id. A follow-up $project maps category: "$_id" and sets _id: 0 so dashboards and exports get human-readable field names.

🧠 How $project Works

1

Document arrives

Each document from the previous stage enters $project with its full field set.

Input
2

Fields evaluated

MongoDB applies inclusion/exclusion rules and evaluates expressions for each output key.

Transform
3

New shape built

A slimmer or renamed document is emitted—only the projected fields appear in output.

Output
=

Shaped stream

Downstream stages and clients receive documents in the exact structure you defined.

📝 Notes

  • Do not mix inclusion (1) and exclusion (0) on regular fields—except _id: 0 in inclusion mode.
  • _id is included by default in inclusion projections unless you set _id: 0.
  • Renaming uses a string path: newKey: "$oldKey"—not newKey: 1.
  • For adding fields without dropping others, prefer $addFields or $set.
  • Early $project can reduce memory in long pipelines by dropping large embedded arrays early.
  • Previous topic: $planCacheStats. Next: $redact.

Conclusion

$project is how you control what leaves a pipeline: whitelist public fields, hide secrets, rename keys for APIs, and compute derived columns—all in one stage.

Pair it with $match and $group for full analytics flows. When you only need to add fields, use $addFields. Next: $redact for conditional document trimming.

💡 Best Practices

✅ Do

  • Set _id: 0 when returning data to external APIs
  • Use inclusion mode for strict public-field allowlists
  • Project after $group to rename _id into meaningful keys
  • Drop large unused fields early to save memory in long pipelines
  • Use $addFields when you only need one or two extra columns

❌ Don’t

  • Mix 1 and 0 on non-_id fields in one $project
  • Assume exclusion mode hides _id—it is kept unless _id: 0
  • Use $project when $addFields would be simpler (adding only)
  • Forget that unlisted fields disappear in inclusion mode
  • Expose cost, tokens, or PII—project them out before $out

Key Takeaways

Knowledge Unlocked

Five things to remember about $project

Use these points whenever you shape documents in aggregation pipelines.

5
Core concepts
02

1 vs 0

Include/exclude.

Modes
📝 03

Rename

"$field" paths.

Pattern
04

Expressions

Compute values.

Advanced
📊 05

Post-$group

Flatten _id.

ETL

❓ Frequently Asked Questions

$project reshapes each document in a pipeline—keeping only the fields you specify, excluding others, renaming fields, and computing new values with aggregation expressions. It is the primary stage for controlling output shape before APIs, exports, or downstream stages.
Generally no—except for _id. You either whitelist fields with 1 (include these) or blacklist with 0 (exclude these). Mixing 1 and 0 on regular fields causes an error. _id can always be set to 0 even in inclusion mode.
$project replaces the document shape—you must list fields to keep (inclusion mode) or fields to drop (exclusion mode). $addFields adds fields while keeping all existing ones by default. Use $project when trimming output; use $addFields when adding a few computed columns.
Yes. Any field value can be an expression: { total: { $multiply: ["$price", "$qty"] } }, { label: { $concat: ["$first", " ", "$last"] } }, or { year: { $year: "$createdAt" } }.
Often after $match (to reduce payload early), after $group (to flatten _id into named fields), or before $out/$merge (to strip sensitive columns). Late $project trims data sent to clients; early $project can reduce work in heavy downstream stages.
Similar idea—both select fields—but $project runs inside aggregation and supports full expression syntax. find() projection is limited to include/exclude/rename via simpler syntax; $project can compute derived fields with $multiply, $cond, etc.
Did you know?

Placing $project early in a pipeline—right after $match—can significantly reduce memory when documents contain large arrays or embedded blobs you never use downstream. MongoDB still reads full documents from disk, but later stages process slimmer in-memory shapes. See the official $project docs.

Continue the Stages Series

Shape fields with $project, then learn conditional trimming with $redact.

Next: $redact →

About the author

Mari Selvan M P
Mari Selvan M P 🔗

Developer, cloud engineer, and technical writer

  • Experience 12 years building web and cloud systems
  • Focus Full Stack Development, AWS, and Developer Education

I write practical tutorials so students and working developers can learn by doing—from databases and APIs to deployment on AWS.

5 people found this page helpful